#BeEffective – Zastosowanie wzorca Builder (budowniczego)

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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *