Immutable Objects
An immutable object is a read-only object (you cannot change it’s properties). It’s easy to test and automatically thread-safe. Since it’s properties cannot be changed. The object will never have an invalid state.
Follow this strategy to make a class immutable:
- Use a constructor to set ALL properties of the object (it’s immutable, so they cannot be changed later on)
- Fields should be final and private
- Since you never want the state of the object to change, do not define any setter methods
- Never return a reference of a mutable member of the object
- Do not allow methods to be overridden. (use a final class or combination of private constructor and factory pattern)
Below you find an example of a class that can be instantiated as an immutable object. Let’s create a class that represents a star system. A star system that has a name, a number of stars in it, and a number of planets in the system. E.g. our own solar system:
public final class StarSystem { private final String name; private final int numberOfStars; private final List<String> planets; public StarSystem(String name, int numberOfStars, List<String> planets){ this.name = name; this.numberOfStars = numberOfStars; this.planets = planets; } public String getName() { return name; } public int getNumberOfStars() { return numberOfStars; } public List<String> getPlanets() { return new ArrayList<String>(planets); } public String getPlanet(int index){ return planets.get(index); } @Override public String toString(){ return "System: " + name + ",\n numberOfStars: " + numberOfStars + ",\n planets: " + planets.toString(); } }
Builder Pattern
Suppose you start of with a class that has three parameters in the constructor. Then developers all over your company start adding parameters to the constructor. Before you know it the class has dozens of constructor parameters. Every time a parameter is added all users of the constructor need to update their call of the constructor. We could define a new constructor for each parameter that is added. This would start an overgrowth of constructors, which also is bad practice.
Hence, the Builder Pattern. Builder patterns provide a way to build the object step by step (most of the time method chaining). Finally the object is generated with a build() call.
Over time following this pattern will keep your code maintainable.
Let’s now provide the possibility to build a star system using a StarSystemBuilderClass:
public class StarSystemBuilder { private String name; private int numberOfStars; private List<String> planets; public StarSystemBuilder setName(String name){ this.name = name; return this; } public StarSystemBuilder setNumberOfStars(int numberOfStars){ this.numberOfStars = numberOfStars; return this; } public StarSystemBuilder setPlanets(List<String> planets){ this.planets = planets; return this; } public StarSystem build(){ return new StarSystem(name, numberOfStars, planets); } }
As you might have seen, the target and builder class are tightly coupled. On the other hand, this tight coupling makes sure that when you use the builder class, you never have to use the StarSystem constructor directly.
Combining the Immutabe Object and the Builder Pattern
Checkout the following example in which I build our own solar system using the StarSystemBuilder class.
public class Main { public static void main (String... args){ StarSystemBuilder starSystemBuilder = new StarSystemBuilder(); StarSystem solarSystem = starSystemBuilder .setName("Solar System") .setNumberOfStars(1) .setPlanets(Arrays.asList("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")) .build(); System.out.println(solarSystem); } }
Nice one, but we can still improve. We could add the builder class to the same package as our target class or we could even add it as a static inner class in our target class. The latter is what I’ll demonstrate below. I’m now able to make the constructor private, forcing users to use the Builder class to construct an object of the target class.
public final class StellarSystem { private final String name; private final int numberOfStars; private final List<String> planets; //private constructor forces user to instantiate StellarSystem by using StellarSystemBuilder private StellarSystem(StellarSystemBuilder builder){ this.name = builder.name; this.numberOfStars = builder.numberOfStars; this.planets = builder.planets; } public String getName() { return name; } public int getNumberOfStars() { return numberOfStars; } public List<String> getPlanets() { return new ArrayList<String>(planets); } public String getPlanet(int index){ return planets.get(index); } @Override public String toString(){ return "System: " + name + ",\n numberOfStars: " + numberOfStars + ",\n planets: " + planets.toString(); } //Tight coupling between StellarSystem and StellarSystemBuilder //In same class to make sure classes can be quickly updated when one of them is changed public static class StellarSystemBuilder { private String name; private int numberOfStars; private List<String> planets; public StellarSystemBuilder setName(String name){ this.name = name; return this; } public StellarSystemBuilder setNumberOfStars(int numberOfStars){ this.numberOfStars = numberOfStars; return this; } public StellarSystemBuilder setPlanets(List<String> planets){ this.planets = planets; return this; } public StellarSystem build(){ return new StellarSystem(this); } } }
This StellarSystem example is like the SolarSystem example above with one slight difference. Here a Builder object is passed to the constructor of the StellarSystem class. Before we were passing the properties of the builder object, not the builder object itself. Now, when the constructor of the StellarSystem changes, you only have to adapt it in the StellarSystem class itself. And no longer in the build() method of your builder.
Wanna have the code of the full example. Check out the builderPattern package in the second module of this project on Github: https://github.com/Nxtra/OCP-Examples