Wzorce Projektowe: Fabryka cz. 1

Cover Image for Wzorce Projektowe: Fabryka cz. 1
Paweł
Paweł

Co pojawia Ci się w głowie po usłyszeniu słowa "Fabryka" ? Być może na myśl przychodzi Ci moloch z dymiącymi kominami, otoczony murem, gdzie ciężkie maszyny wytwarzają różnego rodzaju produkty.

Ok, ale jak w takim razie ma się to do programowania ? Może nie ma tu dymiących kominów i murów, ale fabryka w kodzie ma takie samo zadanie jak ta prawdziwa, czyli "coś" wyprodukować. Tym czymś są różnego rodzaju obiekty.

Z czego składa się fabryka i jak ją zaimplementować? Dowiesz się z naszego artykułu! Ze względu na rozległość tematu, postanowiliśmy podzielić ten wpis na dwie części. W pierwszej odsłonie skupimy się na wprowadzeniu i omówieniu prostej fabryki, podczas gdy w drugiej części skoncentrujemy się na bardziej zaawansowanych wariantach.

A na co to komu ?

Wyobraźmy sobie sytuację, że prowadzimy firmę, która zajmuje się produkcją samochodów. Do tej pory jej specjalizacją były samochody sportowe. W projekcie zajmującym się kontrolowaniem produkcji klasą odpowiedzialną za tworzenie nowych samochodów sportowych jest CarProduction, która posiada jedną metodą zwracają nowy obiekt SportCar:

class CarProduction { createCar(): SportCar { // createCar implementation return new SportCar(); } }

Z czasem okazało się, że wyniki sprzedaży nie są zadowalające. Zespół analityków zrobił badanie rynku. Okazało się, że obecnym trendem są samochody osobowe typu kombi, chętnie kupowane np. przez rodziny z dziećmi. Dołożenie kolejnego rodzaju samochodu wymagało rozszerzenia kodu obsługującego produkcje. Dołożyliśmy więc model EstateCar, która będzie reprezentować samochód typu kombi. Dodatkowo metoda createCar musi teraz zwracać dwa typy samochodów, SportCar oraz EstateCar, dlatego zdecydowaliśmy się wprowadzić dodatkową warstwę abstrakcji, interfejs Car, który będzie implementowany przez oba modele.

interface Car { horsePower: number; model: string; year: number; }
class SportCar implements Car { horsePower: string; model: string; year: number; topSpeed: number;} class EstateCar implements Car { horsePower: string; model: string; year: number; loadCapacity: number;}

W kolejnym kwartale dostaliśmy nowy raport od analityków. Okazało się, że sytuacja na rynku ponownie uległa zmianie. Obecnie najlepiej sprzedającymi się autami są rodzinno-terenowe SUVy. Nie pozostało nam nic innego jak dołożyć nową klasę, która będzie reprezentować taki typ i rozszerzyć naszą metodę odpowiadająca za tworzenie samochodów.

class SUVCar implements Car { brand: string; model: string; year: number; offRoadCapability: string; ... }
class CarProduction { createCar(type: 'sport' | 'estate' | 'suv'): Car { switch (type) { case 'sport': return new SportCar(300, 'Sport', 2020, 340); case 'estate': return new EstateCar(120, 'Estate', 2022, 500); case 'suv': return new SUVCar(150, 'SUV', 2022, 'excellent'); default: throw new Error('Invalid car type'); } } }

W powyższym przykładzie dodano klasę SUV jako kolejną implementację interfejsu Car. Klasa CarProduction została również zmodyfikowana, aby obsługiwać nowy typ auta SUV.

Zmiana nie wygląda na mocno inwazyjną. Można jednak dostrzec, że z każdym kolejnym nowym typem metoda odpowiadająca za tworzenie aut puchnie, stając się mniej czytelna i trudniejsza w utrzymaniu. Pierwszym krokiem w refaktoryzacji może być wydzielenie logiki tworzenia samochodów do osobnej klasy. Ta zmiana dodatkowo pomoże ukryć logikę tworzenia nowych obiektów. Czyli do akcji wkracza opisywany wzorzec Fabryka.

Fabryka

factory

Fabryka to jeden z popularniejszych i łatwiejszych w implementacji wzorców projektowych. Należy do grupy wzorców kreacyjnych, czyli rozwiązań dla często powtarzających się problemów związanych z tworzeniem obiektów. Fabryka umożliwia tworzenie obiektów poprzez jedną, specjalnie przygotowaną klasę. Ma to na celu odseparowanie logiki tworzenia obiektów od klas, które je wykorzystują. Dzięki temu jest ona zebrana w jednym miejscu. Dodatkowo odbywa się to bez większego ingerowania w klasy klientów.

Jeżeli chodzi o rodzaje fabryk, to możemy wyróżnić:

  • Prosta Fabryka (Simple Factory)
  • Metoda Wytwórcza (Factory Method)
  • Fabryka Abstrakcyjna (Abstract Factory)
  • Statyczna Metoda Fabrykująca (Static Factory Method)

Simple Factory

Ok, zacznijmy refactoring od przeniesienia logiki tworzenia do nowej klasy i nazwijmy ją CarFactory. Klasa ta będzie posiadała jedną metodę odpowiadającą za tworzenie kolejnych typów samochodów. Żeby utworzyć nowy rodzaj samochodu, klasa kliencka będzie musiała przekazać odpowiedni parametr, na podstawie którego zostanie utworzony odpowiedni typ.

class CarFactory { createCar(type: 'sport' | 'estate' | 'suv'): Car { switch (type) { case 'sport': return new SportCar(660, 'Sport', 2020, 340); case 'estate': return new EstateCar(320, 'Estate', 2022, 500); case 'suv': return new SUVCar(400, 'SUV', 2022, 'excellent'); default: throw new Error('Invalid car type'); } } }

Zobaczmy jak będzie wyglądać nasza klasa kliencka CarProduction:

class CarProduction { carFactory: CarFactory; constructor() { this.carFactory = new CarFactory(); } produceCar(type: 'sport' | 'estate' | 'suv'): Car { return this.carFactory.createCar(type); } }

Brawo, właśnie utworzyliśmy Simple Factory. Jest to najprostszy rodzaj wzorca fabryki.

Składa się on z trzech elementów:

  1. Wspólny interfejs, określający cechy każdego z wytwarzanych produktów fabryki
  2. Konkretne klasy produktów, które implementują wspólny interfejs
  3. Klasa fabryki

Poniżej uproszczony diagram UML:

diagram

W klasie fabryki mamy jedną metodę, która służy do tworzenia obiektów, nie udostępniając klientom informacji o klasach i obiektach, które są tworzone. Klient wywołuje metodę fabryki wraz z odpowiednimi parametrami, a ta tworzy odpowiedni obiekt zgodnie z tym, co zostało przekazane. Zaletą prostej fabryki jest to, że ukrywa ona szczegóły implementacji przed klientami, co zwiększa enkapsulacje między warstwami aplikacji.

Wadą prostej fabryki jest to, że nie jest elastyczna i z czasem staje się trudna do rozszerzenia. Jeżeli obiekty będą miały różną logikę tworzenia, to nasza klasa fabryki niepotrzebnie się rozrośnie. Dodatkowo za każdym razem, gdy pojawi się nowy typ do utworzenia, musimy rozszerzyć kod klasy fabryki, co łamie zasadę Open Close. Mówi, ona, że możemy dodać nowe funkcjonalności bez potrzeby zmieniania już istniejącego kodu.

Często prowadzi to do błędów i utrudnień w rozwijaniu systemu. Niektóre źródła całkowicie odrzucają prostą fabrykę jako wzorzec projektowy.

Podsumowanie

Podsumowując, jest to dobry początek w kierunku refaktoryzacji. Tworzenie nowych obiektów zostaje przeniesione w jedno miejsce oraz jego szczegóły zostają ukryte przed klientami. Nie jest to jednak rozwiązanie elastyczne oraz ograniczona rozszerzalność kodu. Dlatego w następnej części pójdziemy o krok dalej, prezentując rozwiązanie, które pomoże nam uniknąć powyższych problemów.