4
\$\begingroup\$

I've recently read the books Clean Code and Effective Java. I am a huge fan of Object-Oriented Java.

I am trying to write as professionally as possible. I have been an Android Developer for 20 months now.

Here is a class that I wrote that is supposed to communicate with Google Static Maps API. Is this code Clean, Effective and SOLID?

Also, there are a few nested classes here, should I create a package out of this or contain it in a single file as it is right now?

import static com.example.constants.GoogleStaticMapsAPIKeys.*;

public final class ParametrizedStaticMapsRequest {

    private static final int DEFAULT_ZOOM_LEVEL = 15;
    private static final int DEFAULT_WIDTH = 2 * SizeInPixels.MIN_WIDTH;
    private static final int DEFAULT_HEIGHT = SizeInPixels.MIN_WIDTH;

    private LatLngPoint centerValue = new LatLngPoint(0, 0);
    private GoogleMapsImageFormat imageFormat = GoogleMapsImageFormat.JPG;
    private GoogleMapsZoomLevel zoomLevel = new GoogleMapsZoomLevel(DEFAULT_ZOOM_LEVEL);
    private SizeInPixels size = new SizeInPixels(DEFAULT_WIDTH, DEFAULT_HEIGHT);

    private ParametrizedStaticMapsRequest() {
        throw new AssertionError();
    }

    private ParametrizedStaticMapsRequest(Builder builder) {
        centerValue = builder.centerValue;
        imageFormat = builder.imageFormat;
        zoomLevel = builder.zoomLevel;
        size = builder.size;
    }

    @Override
    public String toString() {

        final String AMPERSAND = "&";
        final String EQUALS = "=";

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(API_ROOT);
        stringBuilder.append(AMPERSAND);
        stringBuilder.append(PARAMETER_API_KEY);
        stringBuilder.append(EQUALS);
        stringBuilder.append(GOOGLE_STATIC_MAPS_API_KEY);
        stringBuilder.append(AMPERSAND);
        stringBuilder.append(PARAMETER_CENTER);
        stringBuilder.append(EQUALS);
        stringBuilder.append(this.centerValue.toString());
        stringBuilder.append(AMPERSAND);
        stringBuilder.append(PARAMETER_FORMAT);
        stringBuilder.append(EQUALS);
        stringBuilder.append(this.imageFormat.toString());
        stringBuilder.append(AMPERSAND);
        stringBuilder.append(PARAMETER_ZOOM);
        stringBuilder.append(EQUALS);
        stringBuilder.append(this.zoomLevel.toString());
        stringBuilder.append(AMPERSAND);
        stringBuilder.append(PARAMETER_SIZE);
        stringBuilder.append(EQUALS);
        stringBuilder.append(this.size.toString());

        return stringBuilder.toString();
    }

    public static class Builder {
        private LatLngPoint centerValue;
        private GoogleMapsImageFormat imageFormat;
        private GoogleMapsZoomLevel zoomLevel;
        private SizeInPixels size;

        public ParametrizedStaticMapsRequest build() {
            return new ParametrizedStaticMapsRequest(this);
        }

        public Builder imageFormat(GoogleMapsImageFormat imageFormat) {
            this.imageFormat = imageFormat;
            return this;
        }

        public Builder zoomLevel(GoogleMapsZoomLevel zoomLevel) {
            this.zoomLevel = zoomLevel;
            return this;
        }

        public Builder size(SizeInPixels size) {
            this.size = size;
            return this;
        }

        public Builder centerValue(LatLngPoint centerValue) {
            this.centerValue = centerValue;
            return this;
        }

    } // End of Builder

    public enum GoogleMapsImageFormat {
        PNG(PARAMETER_FORMAT_PNG),
        JPG(PARAMETER_FORMAT_JPEG),
        GIF(PARAMETER_FORMAT_GIF);

        private final String text;

        private GoogleMapsImageFormat(final String text) {
            this.text = text;
        }

        @Override
        public String toString() {
            return text;
        }
    }

    private final class GoogleMapsZoomLevel {

        public static final int MIN_ZOOM_LEVEL = 0;
        public static final int MAX_ZOOM_LEVEL = 21;

        private int zoomLevel;

        private static final String ERROR_ZOOM_LEVEL_OUT_OF_BOUNDS = "Zoom level should be within " + MIN_ZOOM_LEVEL + " and " + MAX_ZOOM_LEVEL;

        private GoogleMapsZoomLevel() {
            throw new AssertionError();
        }

        public GoogleMapsZoomLevel(int zoomLevel) {
            if(zoomLevel < 0 || zoomLevel > 21) {
                throw new IllegalArgumentException(ERROR_ZOOM_LEVEL_OUT_OF_BOUNDS);
            }
            this.zoomLevel = zoomLevel;
        }

        public String toString() {
            return String.valueOf(zoomLevel);
        }
    }

    private final class SizeInPixels {

        public static final int MIN_WIDTH = 180;
        public static final int MIN_HEIGHT = 1;

        private static final String ERROR_WIDTH_MUST_BE_VALID = "Width must be more than " + MIN_WIDTH + " and less than " + Integer.MAX_VALUE;
        private static final String ERROR_HEIGHT_MUST_BE_VALID = "Height must be more than " + MIN_HEIGHT + " and less than " + Integer.MAX_VALUE;

        private int width;
        private int height;

        private SizeInPixels(){
            throw new AssertionError();
        }

        public SizeInPixels(int width, int height){
            setWidth(width);
            setHeight(height);
        }

        public void setWidth(int width) {
            if(width <= MIN_WIDTH || width > Integer.MAX_VALUE) {
                throw new IllegalArgumentException(ERROR_WIDTH_MUST_BE_VALID);
            }
            this.width = width;
        }

        private void setHeight(int height) {
            if(height <= MIN_HEIGHT || height > Integer.MAX_VALUE) {
                throw new IllegalArgumentException(ERROR_HEIGHT_MUST_BE_VALID);
            }
            this.height = height;
        }

        @Override
        public String toString() {
            return width + "x" + height;
        }
    }

    private final class LatLngPoint {

        public static final double MIN_LONGITUDE = -180;
        public static final double MAX_LONGITUDE = 180;

        public static final double MIN_LATITUDE = -90;
        public static final double MAX_LATITUDE = 90;

        private static final String ERROR_LONGITUDE_OUT_OF_BOUNDS = "Longitude should be within " + MIN_LONGITUDE + " and " + MAX_LONGITUDE;
        private static final String ERROR_LATITUDE_OUT_OF_BOUNDS = "Latitude should be within " + MIN_LATITUDE + " and " + MAX_LATITUDE;

        private double longitude;
        private double latitude;

        private LatLngPoint() {
            throw new AssertionError();
        }

        public LatLngPoint(double latitude, double longitude){
            setLatitude(latitude);
            setLongitude(longitude);
        }

        private void setLatitude(double latitude) {
            if(latitude < MIN_LATITUDE || latitude > MAX_LATITUDE) {
                throw new IllegalArgumentException(ERROR_LATITUDE_OUT_OF_BOUNDS);
            }
            this.latitude = latitude;
        }

        public void setLongitude(double longitude) {
            if(longitude < MIN_LONGITUDE || longitude > MAX_LONGITUDE) {
                throw new IllegalArgumentException(ERROR_LONGITUDE_OUT_OF_BOUNDS);
            }
            this.longitude = longitude;
        }

        @Override
        public String toString() {
            return latitude + "," + longitude;
        }

    } // End of LatLngPoint

} // End of Class



public final class GoogleStaticMapsAPIKeys {

    private GoogleStaticMapsAPIKeys() {
        throw new AssertionError();
    }

    public static final String GOOGLE_STATIC_MAPS_API_KEY = "asd"; 

    public static final String API_ROOT = "https://maps.googleapis.com/maps/api/staticmap?";

    public static final String PARAMETER_CENTER = "center";
    public static final String PARAMETER_ZOOM = "zoom";
    public static final String PARAMETER_ZOOM_DEFAULT_VALUE = "15";
    public static final String PARAMETER_SIZE = "size"; // example: 500x400
    public static final String PARAMETER_FORMAT = "format"; // PNG, JPEG, GIF
    public static final String PARAMETER_API_KEY = "key";
    public static final String PARAMETER_FORMAT_PNG = "png";
    public static final String PARAMETER_FORMAT_JPEG = "jpg";
    public static final String PARAMETER_FORMAT_GIF = "gif";


}
\$\endgroup\$
4
  • \$\begingroup\$ Do you think I should extract the inner classes and make a package out of this or leave it all in a single file? \$\endgroup\$
    – J. K.
    Commented Apr 11, 2015 at 10:41
  • \$\begingroup\$ You may want to read my answer to this question for some advice on the way you have a couple of your constants named. \$\endgroup\$
    – Blrfl
    Commented Apr 11, 2015 at 11:08
  • \$\begingroup\$ You are right there, but this is URL scheme, its unlikely that ampersands or equals signs are going to ever change in the future, so do you think in this case naming those constants AMPERSAND and EQUALS is okay? Otherwise how would I call them? Also other than the constant naming convention, is there any other feedback you have for my code? Thanks! \$\endgroup\$
    – J. K.
    Commented Apr 11, 2015 at 15:33
  • \$\begingroup\$ This answer on programmers should shed some light on that. TL;DR: Constants should be named for what they do not what they are. \$\endgroup\$
    – RubberDuck
    Commented Apr 11, 2015 at 15:42

1 Answer 1

2
\$\begingroup\$

I'm O.K. with the inner classes, but I tend to use them more than others. I would say that you are "reaching the limit" and if there were many more you would want to split them off.

Consider adding a utility method to do all the repeated &key=value stuff, e.g. something like:

public static StringBuilder addKV(StringBuilder stringBuilder, String key, String value) {
 stringBuilder.append(AMPERSAND);
 stringBuilder.append(key);
 stringBuilder.append(EQUALS);
 stringBuilder.append(value);
 return stringBuilder;
}

In this case, naming the constants AMPERSAND and EQUALS is fine, and probably better than trying to come up with a "what they do" name.

Also, instead of using static import GoogleStaticMapsAPIKeys to save typing, but then adding a "PARAMETER_" to the start of all those key names for clarity, consider giving that class a shorter name, not statically importing, but lose the "PARAMETER_" stuff. e.g.

public class MapsAPIKey {
   public static final String CENTER = "center";
   ... etc...
}

Which would be referred to in the code as MapsAPIKey.CENTER etc... To my eye that is more natural, YMMV.

Addendum Suggestion

Make the fields of ParametrizedStaticMapsRequest final. The fields of the Builder should be initialized to "good defaults", like JPG and DEFAULT_ZOOM_LEVEL, then modified by the user.

If it makes sense, you could then define some statics like

public static final ParametrizedStaticMapsRequest DEFAULT_JPG = new ParametrizedStaticMapsRequest (new Builder());

\$\endgroup\$
7
  • \$\begingroup\$ Thank you for your input. The thought of convenience methods for adding key value pairs had crossed my mind but I dismissed it, now you validate it might actually be a good idea indeed. On static imports, I didnt just use static imports because the name of GoogleStaticMapsAPIKeys class is long, but also because I am excited about static imports, I just learned about them and I think they are very handy :D \$\endgroup\$
    – J. K.
    Commented Apr 11, 2015 at 17:35
  • \$\begingroup\$ Also, should I include !=null checks in the setters for the inner classes and what should I do in those If I did include them? \$\endgroup\$
    – J. K.
    Commented Apr 11, 2015 at 17:38
  • \$\begingroup\$ Yes, checking for null arguments in the Builder and throwing an IllegalArgumentException or similar is a good idea. (Or else, set to a "good default") \$\endgroup\$
    – user949300
    Commented Apr 11, 2015 at 18:43
  • \$\begingroup\$ On the appendum suggestion. Im already setting the fields in ParametrizedStaticMapsRequest to good defaults. Should I move these initializations to Builder, and just make those fields final in ParametrizedStaticMapsRequest and not initialize them at all there? \$\endgroup\$
    – J. K.
    Commented Apr 11, 2015 at 21:07
  • 1
    \$\begingroup\$ Almost all of my inner classes are static. \$\endgroup\$
    – user949300
    Commented Apr 13, 2015 at 16:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.