W pierwszym wpisie z cyklu #BeEffective poruszony został temat statycznych metod fabrycznych i konstruktorów. Posiadają one jednak pewną wspólną wadę – nie są optymalne do obsługi wielu parametrów opcjonalnych. Rozważmy przypadek klasy reprezentującej etykiety z informacją o składnikach odżywczych. Taka etykieta z pól wymaganych posiada wielkość porcji, ilość porcji na opakowanie oraz liczbę kalorii na 100g produktu. Posiada także wiele pól opcjonalnych – od prostego podziału węglowodany / białka / tłuszcze, po rozbite na części pierwsze wszystkie mikro i makroelementy. Większość produktów niezerowe wartości ma tylko dla kilku z pól opcjonalnych.
Zazwyczaj w takiej sytuacji programiści decydują się na skorzystanie z tzw. konstruktora teleskopowego – tworzy się konstruktor tylko z polami wymaganymi, a następnie kolejny z jednym polem opcjonalnym, dwoma, trzema itd. Wygląda to mniej więcej tak:
public class Nutrition {
private final int servingSize;
private final int servingsAmount;
private final int kcal;
private int fat;
private int carbohydrate;
private int protein;
public Nutrition(int servingSize, int servingsAmount, int kcal){
this.servingSize = servingSize;
this.servingsAmount = servingsAmount;
this.kcal = kcal;
}
public Nutrition(int servingSize, int servingsAmount, int kcal, int fat){
this(servingSize, servingsAmount, kcal);
this.fat = fat;
}
public Nutrition(int servingSize, int servingsAmount, int kcal, int fat, int carbohydrate){
this(servingSize, servingsAmount, kcal, fat);
this.carbohydrate = carbohydrate;
}
public Nutrition(int servingSize, int servingsAmount, int kcal, int fat, int carbohydrate, int protein){
this(servingSize, servingsAmount, kcal, fat, carbohydrate);
this.protein = protein;
}
}
Niby wszystko spoko. Co jednak, jeśli chcemy stworzyć etykietę batonika białkowego, w którym nieistotna jest ilość tłuszczu i cukru, a jedynie białka? Są na to dwa sposoby:
Nutrition proteinBar1 = new Nutrition(100, 3, 150); proteinBar1.setProtein(25); Nutrition proteinBar2 = new Nutrition(100, 3, 150, null, null, 25);
Wzorzec konstruktora teleskopowego zatem działa (czasem niespecjalnie elegancko, ale działa). Ciężko jednak używać kod aplikacji napisanej w ten sposób, a dodatkowo zwiększa się nieczytelność kodu. Zorientowanie się w konkretnych parametrach wymaga skrupulatnego liczenia pozycji, a o pomyłkę bardzo łatwo. Drugie podejście prezentuje pewną hybrydę wzorca teleskopu ze wzorcem JavaBeans, który w pełni zastosowany wyglądałby następująco:
Nutrition proteinBar3 = new Nutrition(); proteinBar3.setServingSize(100); proteinBar3.setServingsAmount(3); proteinBar3.setKcal(150); proteinBar3.setProtein(25);
Czytelność jest wyraźnie zwiększona, ale tworzenie większych obiektów zajmowałoby bardzo dużo miejsca. Dodatkowo, z racji tego, że tworzenie obiektu wymaga wywołania wielu instrukcji jednej po drugiej – do momentu zakończenia tworzenia obiekt znajduje się w stanie niespójnym. Klasa nie ma możliwości wymuszenia spójności obiektu poprzez sprawdzenie wartości parametrów konstruktora, a próba użycia niespójnego obiektu może wywołać awarię systemu.
Istnieje jednak alternatywa, łącząca bezpieczeństwo tworzenia spójnego obiektu z wzorca teleskopu jak i czytelność wzorca JavaBeans. Jest to tak zwany wzorzec Budowniczego (Builder). We wzorcu tym zamiast bezpośrednio tworzyć wymagany obiekt, wywoływany jest konstruktor / metoda fabryczna z polami obowiązkowymi zwracająca obiekt budowniczego. Następnie wywoływane są kolejne metody uzupełniające pola opcjonalne, a na koniec wywoływana jest bezargumentowa metoda budująca i zwracająca nowoutworzony obiekt. Dla naszej klasy wyglądałoby to następująco:
public class Nutrition {
private final int servingSize;
private final int servingsAmount;
private final int kcal;
private int fat;
private int carbohydrate;
private int protein;
private Nutrition(Builder builder){
servingSize = builder.servingSize;
servingsAmount = builder.servingsAmount;
kcal = builder.kcal;
fat = builder.fat;
carbohydrate = builder.carbohydrate;
protein = builder.protein;
}
public static class Builder{
private final int servingSize;
private final int servingsAmount;
private final int kcal;
private int fat;
private int carbohydrate;
private int protein;
public Builder(int servingSize, int servingsAmount, int kcal) {
this.servingSize = servingSize;
this.servingsAmount = servingsAmount;
this.kcal = kcal;
}
public Builder withFat(int fat){
this.fat = fat;
return this;
}
public Builder withCarbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public Builder withProtein(int protein){
this.protein = protein;
return this;
}
public Nutrition build(){
return new Nutrition(this);
}
}
}
Kod kliencki tworzenia naszego batonika wyglądałby w tej sytuacji następująco:
Nutrition proteinBar4 = new Nutrition.Builder(100, 3, 150).withProtein(25).build();
Taki kod jest zrozumiały, łatwy do napisania oraz czytania. Aby zapewnić poprawność parametrów przesyłanych do utworzenia obiektu można przeprowadzić walidację wewnątrz konstruktora oraz metod budujących budowniczego.
W następnym (krótkim) wpisie poruszę temat wzorca budowniczego wykorzystanego w hierarchii klas.