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.