Nowy cykl – #BeEffective – Statyczne metody fabryczne zamiast konstruktorów

Dość późno w mojej karierze dotarłem do książki, którą każdy middev powinien przeczytać, bo dopiero teraz. Ten tytuł to oczywiście Java. Efektywne programowanie od Joshuy Blocha. Tym samym otwieram cykl #BeEffective, w którym omawiać będę kolejne lekcje tego podręcznika. W końcu, jak stoi w przedmowie tejże książki – na podstawie tej książki możesz zaprojektować własny kurs.

Najczęstszym sposobem na tworzenie nowych obiektów jest wykorzystanie publicznego konstruktora. Niewielu programistów zdaje sobie sprawę z tego, że to samo można uzyskać także w inny sposób – za pomocą statycznej metody fabrycznej. Jest to jak sama nazwa wskazuje, metoda statyczna, która przejmuje obowiązki konstruktora i zwraca stworzony obiekt. Na przykładzie typu Boolean:

public static Boolean valueOf(boolean b){
    return (b ? Boolean.TRUE : Boolean.FALSE);
}

Stosowanie wyżej wymienionych metod ma zarówno zalety, jak i wady. Na listę plusów takiego rozwiązania można między innymi zapisać to, że metody fabryczne mogą po prostu posiadać nazwy. Potrafi to ułatwić korzystanie z klasy oraz uczynić kod danej klasy prostszym w analizie. Dla przykładu – BigInteger ma konstruktor przyjmujący jako argumenty dwie liczby całkowite, oraz obiekt java.util.Random. Wg javadoc – tworzy on obiekt BigInteger z liczbą, która jest prawdopodobnie pierwsza. Teraz zwróćmy uwagę na metodę probablePrime. Prawda, że jest bardziej przejrzyste?

Klasa może posiadać tylko jeden konstruktor o określonej liczbie i kolejności argumentów. Programiści obchodzą to, tworząc kolejne konstruktory z więskzą ich ilością, co jest nienajlepszą techniką. Pisząc kod należy zawsze myśleć o użytkownikach klas przez nas pisanych, a ci ostatni często nie są w stanie zgadnąć jaki konstruktor do czego służy, więc muszą posiłkować się bezpośrednio kodem klasy. Posiadanie nazw przez metody fabryczne umożliwia wykorzystanie takiej samej ilości parametrów w kilku przypadkach, a ich użycie (dzięki odpowiednim nazwom metod) może być intuicyjne.

Drugą zaletą wykorzystania metod fabrycznych jest to, że nie muszą one tworzyć obiektu. Pozwala to tworzyć klasy niezmienne, korzystające z wstępnie utworzonej puli obiektów, lub w sobie znany sposób zarządzające procesem tworzenia nowych obiektów. Dla przykładu – metoda Boolean.valueOf(boolean) nigdy nie tworzy nowego obiektu. Jeśli obiekty są często tworzone, dzięki takiej technice możliwe jest znaczne poprawienie wydajności aplikacji, szczególnie, jeśli tworzenie obiektów wymaga dużych nakładów czasowych. Można tę zdolność także wykorzystać do ścisłej kontroli nad istniejącymi w danym momencie instancjami. Klasy takie nazywa się kontrolowanymi przez instancję, a powody tworzenia takich klas są dwa:

  • pozwala to na zagwarantowanie, że klasa jest singletonem
  • pozwala to klasie niezmiennej upewnić się, że nie istnieją dwa identyczne obiekty (a.equals(b) wtedy i tylko wtedy, gdy a == b)

Ciekawostką jest fakt, że powyższa właściwość jest podstawą wzorca Flyweight, a tego typu optymalizacja stosowana jest między innymi w enumach.

Trzecia z zalet statycznych metod fabrycznych jest możliwość zwracania typu będącego podtypem zdefiniowanego, zwracanego typu. Pozwala to na zwracanie obiektów, których klasy nie są klasami publicznymi. Pozwala to na tworzenie czegoś w rodzaju API i wykorzystywane jest w bibliotekach, korzystających z interfejsów. W podejściu takim statyczne metody fabryczne korzystają z typów zdefiniowanych w interfejsie.

Kolejną zaletą jest to, że w zależności od rodzajów argumentów przekazywanych do metody, zmianie może ulegać klasa zwracanego obiektu. To proste – konstruktor zawsze tworzy obiekt swojej klasy, natomiast w przypadku metod – nawet deklarując metodę jako zwracającą obiekt danej klasy, można przecież zwrócić obiekt klasy pochodnej. Jako przykład rozważmy klasę EnumSet – nie ma ona publicznych konstruktorów, a jedynie statyczne metody fabryczne. Metody te zwracają jedną z dwóch implementacji, w zalezności od rozmiaru typu wyliczeniowego – jeśli ma on najwyżej 64 elementy (jak w sumie większość wykorzystywanych w programowaniu enumów), metoda zwraca obiekt typu RegularEnumSet, bazujący na jednej liczbie typu long. Jeśli typ wyliczeniowy natomiast zawiera więcej elementów – zwracany jest obiekt JumboEnumSet, bazujący na tablicy takich liczb. Szczegóły tutaj, tutaj i tutaj (implementacja OpenJDK).

Istnienie tych dwóch implementacji jest teoretycznie niewidoczne dla klienta. Jeśli takie rozgraniczenie nie dawałoby zysku wydajnościowego, można by usunąć te dwie klasy w kolejnych bibliotekach języka bez efektów ubocznych. Tak samo w drugą stronę – w kolejnej wersji można wprowadzić kolejne implementacje abstrakcji EnumSet, która byłaby w stanie poprawić wydajność aplikacji klienckiej.

Pisanie takich metod, jak każda technika programowania, oprócz zalet ma także wady. Jedną z nich jest to, że całkowite pominięcie konstruktorów publicznych uniemożliwia nam dziedziczenie po klasach. Powoduje to często skorzystanie z kompozycji zamiast dziedziczenia, nawet jeśli nie byłaby to najlepsza droga napisania danego fragmentu kodu.

Drugą istotną wadą jest to, że statycznych metod fabrycznych nie sposób odróżnić od zwykłych statycznych metod. W wygenerowanej dokumentacji znajdą się one nie w towarzystwie konstruktorów, a pośród normalnych metod, co może utrudniać użytkownikowi zrozumienie sposobu tworzenia obiektów. Tę niedogodność można zmniejszyć korzystając z konwencji nazewnictwa metod fabrycznych, oraz intuicyjności nazw.

Podsumowując – metody fabryczne oraz konstruktory mają swoje zastosowania i warto poznać zalety jak i wady obu tych rozwiązań. Zaleca się stosowanie metod fabrycznych nad konstruktory, jednak odbywa się to kosztem możliwości dziedziczenia. Na szczęście oba rozwiązania mogą istnieć obok siebie, możliwe jest zatem stworzenie pustego domyślnego konstruktora, oraz szeregu metod fabrycznych realizujących tworzenie obiektu.

Dodaj komentarz

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