Continuing the series of entries about design patterns, I decided to take a builder design pattern to the workshop. This pattern is a creative pattern that can significantly simplify the application code, make it more transparent, and at the same time make us get a very simple tool for developing our application. I invite you to read.
Spis Treści
When and why exactly the builder?
The builder pattern can be used when we need to implement in our application the creation of objects that are structurally very similar to each other, but have different implementations. In addition, this pattern allows you to create objects in a stepwise manner. It is best if we try to present the problem with a simple example of creating a bolt object.
The screw seems to be a very simple item, but the multitude of its types makes it not easy to create one class that would be able to easily describe many of its types. Let's take an example of the features that a screw can have:
- thread size
- thread type
- thread length
- bolt length
- head shape
- coating
- type of coating
- strength class,
- left or right-handedness,
- thread pitch,
- the material from which it is made,
- etc.
If we wanted to create an example constructor of our screw, we would have a nice monster that could look like this.
Sruba srubaPierwsza = new Sruba(6, "metryczna", 20, "walcowy z gniazdem sześciokątnym", "ocynk", "8.8", "prawoskretna", "normalny");
One such constructor could get over and overwork with filling it, but what to do if we would like to use many screws that would only differ in length, eg M6x15, M6x20, M6x30. It would be best if someone or something would fill such a constructor for us.
Let's build a builder
So let's create a builder class for our screw-class facilities.
Bolt class
package com.company;
public class Sruba {
private double rozmiarGwintu;
private String typ;
private double dlugosc;
private String typLba;
private String pokrycie;
private String klasa;
private String skretnosc;
private String skok;
//setters and getters
}
Bulider class
package com.company;
public class SrubaTyp1 {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc){
sruba = new Sruba();
sruba.setRozmiarGwintu(rozmiar);
sruba.setTyp("metryczna");
sruba.setDlugosc(dlugosc);
sruba.setTypLba("walcowy z gniazdem sześciokątnym");
sruba.setPokrycie("ocynk");
sruba.setKlasa("8.8");
sruba.setSkretnosc("prawoskrestna");
sruba.setSkok("normalny");
}
public Sruba getSruba() {
return sruba;
}
}
Note that now to create multiple type 1 bolts, all you need to do is create a type 1 bolt builder object and a Bolt class object to which we will assign the result of the builder's work as in the example below:
package com.company;
public class Main {
public static void main(String[] args) {
SrubaTyp1 budowniczy1 = new SrubaTyp1(6,20);
Sruba sruba1 = budowniczy1.getSruba();
budowniczy1.setDlugoscSrubby(25); //zmieniam tylko długość
Sruba sruba2 = budowniczy1.getSruba(); //tworzę kolejną śrubę o nowej dlugosci
}
}
See how elegant solution we have received. Instead of creating many long, hardly readable constructors, we created a class that creates objects of the Bolt class for us. Similarly to the SrubaTyp1 class, you can create subsequent SrubaTyp2, SrubaTyp3 classes, etc. To ensure consistency and correct creation of the Sruba class objects, let's create an interface that will have to be implemented in each builder class of the Sruba class.
package com.company;
public interface SrubaTypy {
public void setRozmiarSruby(double rozmiarSruby);
public void setTyp(String typ);
public void setDlugosc(double dlugosc);
public void setTypLba(String typLba);
public void setPokrycie(String pokrycie);
public void setKlasa(String klasa);
public void setSkretnosc(String skretnosc);
public void setSkok(String skok);
}
Having such an interface, I change the SrubaTyp1 class so that it implements the interface written above:
package com.company;
public class SrubaTyp1 implements SrubaTypy {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc) {
sruba = new Sruba();
setRozmiarSruby(rozmiar);
setTyp("metryczna");
setDlugosc(dlugosc);
setTypLba("walcowy z gniazdem sześciokątnym");
setPokrycie("ocynk");
setKlasa("8.8");
setSkretnosc("prawoskrestna");
setSkok("normalny");
}
public Sruba getSruba() {
return sruba;
}
@Override
public void setRozmiarSruby(double rozmiar) {
sruba.setRozmiarGwintu(rozmiar);
}
@Override
public void setTyp(String typ) {
sruba.setTyp(typ);
}
@Override
public void setDlugosc(double dlugosc) {
sruba.setDlugosc(dlugosc);
}
@Override
public void setTypLba(String typLba) {
sruba.setTypLba(typLba);
}
@Override
public void setPokrycie(String pokrycie) {
sruba.setPokrycie("ocynk");
}
@Override
public void setKlasa(String klasa) {
sruba.setKlasa(klasa);
}
@Override
public void setSkretnosc(String skretnosc) {
sruba.setSkretnosc(skretnosc);
}
@Override
public void setSkok(String skok) {
sruba.setSkok(skok);
}
}
Notice that now we have a situation where the constructor creates the Bolts object for us and automatically fills in all the fields. In addition, we have the ability to freely change the values of Bolt fields through setters in the builder class. In the present situation, it is the builder himself who oversees the process of creating the bolt.
Let's put a manager on the builder
A builder pattern often uses an additional manager class to oversee the steps and sequence of using builder class methods. This approach makes the code clearer and easier to operate with builders. Let's write down an example of a manager's code:
package com.company;
public class Kierownik {
public void stworzSrubeTypuPierwszego(SrubaTypy budowniczySruby) {
budowniczySruby.setDlugosc(20);
budowniczySruby.setRozmiarSruby(6);
budowniczySruby.setTypLba("Stożkowy z gniazdem sześciokątnym");
}
public void stworzSrubeTypuDrugiego(SrubaTypy budowniczySruby) {
budowniczySruby.setDlugosc(30);
budowniczySruby.setKlasa("8.8");
budowniczySruby.setRozmiarSruby(8);
}
}
Having a manager who instructs how to create an object of the bolt class, you can slim down the builder builder and then the class will look like this:
package com.company;
public class SrubaTyp1 implements SrubaTypy {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc) {
sruba = new Sruba();
}
public Sruba getSruba() {
return sruba;
}
@Override
public void setRozmiarSruby(double rozmiar) {
sruba.setRozmiarGwintu(rozmiar);
}
@Override
public void setTyp(String typ) {
sruba.setTyp(typ);
}
@Override
public void setDlugosc(double dlugosc) {
sruba.setDlugosc(dlugosc);
}
@Override
public void setTypLba(String typLba) {
sruba.setTypLba(typLba);
}
@Override
public void setPokrycie(String pokrycie) {
sruba.setPokrycie("ocynk");
}
@Override
public void setKlasa(String klasa) {
sruba.setKlasa(klasa);
}
@Override
public void setSkretnosc(String skretnosc) {
sruba.setSkretnosc(skretnosc);
}
@Override
public void setSkok(String skok) {
sruba.setSkok(skok);
}
}
We gained a lighter builder class constructor, and also separated the bolt creation methodology into a separate class, which gave us clarity and ease in building subsequent implementations of different types of bolt in one place by adding new methods in the manager class. In addition, at this point, we have enabled the user to implement his own order of building the object, and in addition, we can now introduce, for example, conditional instructions in the manager's class, which will supervise the subsequent stages of creating the bolt object.
To use this class structure, let's rebuild our client code:
package com.company;
public class Main {
public static void main(String[] args) {
Kierownik kierownik = new Kierownik(); // zatrudniam do pracy kierownika
SrubaTypy budowniczySrubyPierwszy = new SrubaTyp1(); // zatrudniam pierwszego budowniczego
SrubaTypy budowniczySrubyDrugi = new SrubaTyp1(); // zatrudniam drugiego budowniczego
kierownik.stworzSrubeTypuPierwszego(budowniczySrubyPierwszy); //przypisuję do stowrzenia sruby konkretnego budowniczego
kierownik.stworzSrubeTypuDrugiego(budowniczySrubyDrugi); //przypisuję do stowrzenia sruby konkretnego budowniczego
Sruba pierwszaSruba = budowniczySrubyPierwszy.getSruba(); // przypisuje do pierwszaSruba efekt pracy budowniczego pierwszego
Sruba drugaSruba = budowniczySrubyDrugi.getSruba(); // przypisuje do drugaSruba efekt pracy budowniczego drugiego
System.out.println("---SRUBA PIERWSZA---");
System.out.println("Rozmiar gwintu: " + pierwszaSruba.getRozmiarGwintu());
System.out.println("Typ: " + pierwszaSruba.getTyp());
System.out.println("Dlugosc: " + pierwszaSruba.getDlugosc());
System.out.println("Typ Lba: " + pierwszaSruba.getTypLba());
System.out.println("Pokrycie: " + pierwszaSruba.getPokrycie());
System.out.println("Klasa: " + pierwszaSruba.getKlasa());
System.out.println("Skretnosc: " + pierwszaSruba.getSkretnosc());
System.out.println("Skok: " + pierwszaSruba.getSkok());
System.out.println("---SRUBA DRUGA---");
System.out.println("Rozmiar gwintu: " + drugaSruba.getRozmiarGwintu());
System.out.println("Typ: " + drugaSruba.getTyp());
System.out.println("Dlugosc: " + drugaSruba.getDlugosc());
System.out.println("Typ Lba: " + drugaSruba.getTypLba());
System.out.println("Pokrycie: " + drugaSruba.getPokrycie());
System.out.println("Klasa: " + drugaSruba.getKlasa());
System.out.println("Skretnosc: " + drugaSruba.getSkretnosc());
System.out.println("Skok: " + drugaSruba.getSkok());
}
}
The result of starting the program will be as follows:
---SRUBA PIERWSZA---
Rozmiar gwintu: 6.0
Typ: null
Dlugosc: 20.0
Typ Lba: Stożkowy z gniazdem sześciokątnym
Pokrycie: null
Klasa: null
Skretnosc: null
Skok: null
---SRUBA DRUGA---
Rozmiar gwintu: 8.0
Typ: null
Dlugosc: 30.0
Typ Lba: null
Pokrycie: null
Klasa: 8.8
Skretnosc: null
Skok: null
Process finished with exit code 0
Summary
The Builder Pattern is a moderately complex design pattern, but when used wisely, it adds flexibility to the application and allows the developer to create objects in stages. In addition, the client code is significantly simplified and more transparent, which results, for example, from the lack of the need to create complex and extensive constructors.
The main disadvantage of this pattern is that it requires the creation of additional classes and interfaces, which in some projects may unnecessarily complicate its class structure.