Przedstawiam Wam serię artykułów poświęconych wzorcom projektowym. W pierwszym z nich, jak w tytule, opiszę na prostym przykładzie wzorzec Fabryka Abstrakcyjna.
Rzućmy okiem na definicję:
„Rusz głową! Wzorce Projektowe”
Co to oznacza w praktyce? Otóż pisząc kod, który wykorzystuje interfejs, mamy możliwość odseparowania kodu naszego programu (klienta) od fabryki tworzącej odpowiednie produkty. Dzięki takiemu rozwiązaniu możemy implementować wiele różnych fabryk produkujących wyroby przeznaczone dla różnych podmiotów — takich jak regiony, systemy operacyjne, różni odbiorcy.
To tyle z bardzo krótkiego wstępu teoretycznego . Przejdźmy od razu do praktycznego przykładu, który przedstawię w języku Java.
Fabryka abstrakcyjna — praktyczny przykład w języku Java
Załóżmy, że mamy dwa bary z kebabem: JanuszexKebab i Kebab u Zdzicha. Ten pierwszy sprzedaje kebaby w picie z sosem mieszanym, drugi natomiast w grubym cieście z sosem ostrym (tak, ja wiem, że w większości kebabiarnii istnieje możliwość wyboru sosu, ale na potrzeby tego artykułu postanowiłem uprościć przykład i tym samym zredukować ilość kodu ).
W naszym przypadku rodziną produktów są składniki niezbędne do przyrządzenia kebaba. Oto interfejs określający sposób tworzenia tychże właśnie składników — to właśnie on posłuży nam jako fabryka abstrakcyjna:
public interface KebabFactory {
Dough createDough();
ChickenMeat createChickenMeat();
MuttonMeat createMuttonMeat();
VeganStuffing createVeganStuffing();
Veggies[] createVeggies();
Sauce createSauce();
}
Nasza fabryka będzie tworzyć instancje klas odpowiadających poszczególnym składnikom „kebsa”.
Zobaczmy na implementacje powyższego interfejsu — najpierw według Janusza:
public class JanuszexKebabFactory implements KebabFactory {
@Override
public Dough createDough() {
return new PitaDough();
}
@Override
public ChickenMeat createChickenMeat() {
return new SlicedChickenMeat();
}
@Override
public MuttonMeat createMuttonMeat() {
return new SlicedMuttonMeat();
}
@Override
public VeganStuffing createVeganStuffing() {
return new SoyCutlets();
}
@Override
public Veggies[] createVeggies() {
Veggies veggies[] = {
new Tomato(),
new Cucumber(),
new Salad()
};
return veggies;
}
@Override
public Sauce createSauce() {
return new MixedSauce();
}
}
A teraz według Zdzicha:
public class KebabUZdzichaFactory implements KebabFactory {
@Override
public Dough createDough() {
return new BunDough();
}
@Override
public ChickenMeat createChickenMeat() {
return new SlicedChickenMeat();
}
@Override
public MuttonMeat createMuttonMeat() {
return new SlicedMuttonMeat();
}
@Override
public VeganStuffing createVeganStuffing() {
return new SoyCutlets();
}
@Override
public Veggies[] createVeggies() {
Veggies veggies[] = {
new Cucumber(),
new Onion(),
new Tomato(),
new Salad()
};
return veggies;
}
@Override
public Sauce createSauce() {
return new SpicySauce();
}
}
Powyższe implementacje określane są mianem fabryk rzeczywistych (konkretnych). Aby utworzenie danego produktu było możliwe, klient ma za zadanie wykorzystać jedną z tych fabryk, czyli nigdy nie będzie mu dane samodzielne tworzenie instancji obiektów będących reprezentantami produktów.
Każda fabryka rzeczywista może produkować całe zestawy produktów.
Ponieważ niektórym początkującym programistom zdarza się mylić ten wzorzec ze wzorcem Metoda Fabrykująca (ang. Factory Method), postanowiłem w tym przykładzie skorzystać z drugiego wzorca, który posłuży nam do tworzenia kebabów ze składników powstających w Fabryce Abstrakcyjnej.
Łączenie Fabryki Abstrakcyjnej z Metodą Fabrykującą (Wytwórczą)
Podstawową różnicą między Fabryką Abstrakcyjną a Metodą Fabrykującą jest to, że ta druga zapewnia abstrakcyjny interfejs tylko JEDNEGO produktu. Ponadto każda klasa podrzędna jest odpowiedzialna za decyzję odnośnie wyboru, której klasy rzeczywistej zostanie utworzony obiekt.
Naszą metodą fabrykującą będzie część barowa Januszeksu i Kebaba u Zdzicha, w której kebaby będą podawane klientom.
Tym razem skorzystamy z klasy abstrakcyjnej.
public abstract class KebabBar {
protected abstract Kebab createKebab(String type);
public Kebab orderKebab(String type) {
Kebab kebab = createKebab(type);
System.out.println("--- Przyrządzanie kebaba: " + kebab.getName() + " ---");
kebab.prepare();
kebab.getIngredients();
kebab.bake();
kebab.box();
return kebab;
}
}
Dzięki zastosowaniu metody fabrykującej każda kebabiarnia otrzymuje swoją fabrykę rzeczywistą ze sposobami produkcji lokalnych odmian kebabów. Parametr type jest słowem kluczowym, które pozwoli klasie podrzędnej wybrać klasę rzeczywistą do utworzenia instancji, czyli produktu konkretnego (rzeczywistego).
Utwórzmy teraz interfejs produktu, który również będzie klasą abstrakcyjną.
public abstract class Kebab {
String name;
Dough dough;
ChickenMeat chickenMeat;
MuttonMeat muttonMeat;
VeganStuffing veganStuffing;
Sauce sauce;
Veggies veggies[];
abstract void prepare();
void getIngredients() {
System.out.println("Dodawanie składników do ciasta");
}
void bake() {
System.out.println("Podpiekanie kebaba w opiekaczu (2-3 min.)");
}
void box() {
System.out.println("Zapakowanie kebaba do opakowania");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("---- " + name + "----\n");
if (dough != null) {
result.append(dough);
result.append("\n");
}
if (chickenMeat != null) {
result.append(chickenMeat);
result.append("\n");
}
if (muttonMeat != null) {
result.append(muttonMeat);
result.append("\n");
}
if (veganStuffing != null) {
result.append(veganStuffing);
result.append("\n");
}
if (sauce != null) {
result.append(sauce);
result.append("\n");
}
if (veggies != null) {
for (int i = 0; i < veggies.length; i++) {
result.append(veggies[i]);
if (i < veggies.length - 1) {
result.append(", ");
}
}
result.append("\n");
}
return result.toString();
}
}
Przyszła pora na produkty rzeczywiste. W naszym przykładzie zostaną utworzone trzy wyroby: kebab z wołowiną, kebab z kurczakiem oraz kebab wegański z kotletami sojowymi zamiast mięsa (załóżmy, że ciasto również będzie wegańskie, mimo że w kodzie nie będzie to wyraźnie uwzględnione).
public class MuttonKebab extends Kebab {
KebabFactory kebabFactory;
public MuttonKebab(KebabFactory kebabFactory) {
this.kebabFactory = kebabFactory;
}
@Override
void prepare() {
System.out.println("Przygotowywanie: " + name);
dough = kebabFactory.createDough();
muttonMeat = kebabFactory.createMuttonMeat();
veggies = kebabFactory.createVeggies();
sauce = kebabFactory.createSauce();
}
}
Każda klasa opisująca dany typ kebaba dostaje gotową fabrykę, którą przekazujemy w konstruktorze klasy i przechowujemy w odpowiedniej zmiennej obiektowej. Metoda prepare() realizuje kolejne kroki tworzenia kebaba i za każdym razem, gdy potrzebny jest taki czy inny składnik, fabryka wykonuje go i dostarcza do klasy.
Pozostałe klasy tworzące kebaby (ChickenKebab i VeganKebab) będą wyglądały podobnie. Jedyna różnica będzie polegać na wywołaniu metody, która tworzy „nadzienie” do kebabów.
public class ChickenKebab extends Kebab {
KebabFactory kebabFactory;
public ChickenKebab(KebabFactory kebabFactory) {
this.kebabFactory = kebabFactory;
}
@Override
void prepare() {
System.out.println("Przygotowywanie " + name);
dough = kebabFactory.createDough();
chickenMeat = kebabFactory.createChickenMeat();
veggies = kebabFactory.createVeggies();
sauce = kebabFactory.createSauce();
}
}
public class VeganKebab extends Kebab {
KebabFactory kebabFactory;
public VeganKebab(KebabFactory kebabFactory) {
this.kebabFactory = kebabFactory;
}
@Override
void prepare() {
System.out.println("Przygotowywanie: " + name);
dough = kebabFactory.createDough();
veganStuffing = kebabFactory.createVeganStuffing();
veggies = kebabFactory.createVeggies();
sauce = kebabFactory.createSauce();
}
}
Nadszedł czas na implementacje naszej metody fabrykującej.
Najpierw bar u Janusza:
public class JanuszexKebabBar extends KebabBar {
@Override
protected Kebab createKebab(String type) {
Kebab kebab = null;
KebabFactory kebabFactory = new JanuszexKebabFactory();
if (type.equals("baranina")) {
kebab = new MuttonKebab(kebabFactory);
kebab.setName("Kebab w picie z baraniną");
} else if (type.equals("kurczak")) {
kebab = new ChickenKebab(kebabFactory);
kebab.setName("Kebab w picie z kurczakiem");
} else if (type.equals("wegański")) {
kebab = new VeganKebab(kebabFactory);
kebab.setName("Wegański kebab w picie");
}
return kebab;
}
}
Teraz u Zdzisława:
public class KebabUZdzichaBar extends KebabBar {
@Override
protected Kebab createKebab(String type) {
Kebab kebab = null;
KebabFactory kebabFactory = new KebabUZdzichaFactory();
if (type.equals("baranina")) {
kebab = new MuttonKebab(kebabFactory);
kebab.setName("Kebab w bułce z baraniną");
} else if (type.equals("kurczak")) {
kebab = new ChickenKebab(kebabFactory);
kebab.setName("Kebab w bułce z kurczakiem");
} else if (type.equals("wegański")) {
kebab = new VeganKebab(kebabFactory);
kebab.setName("Wegański kebab w bułce");
}
return kebab;
}
}
Mamy już wszystko dopięte na ostatni guzik, więc pora przetestować nasz program.
Test przykładowego programu
W klasie testowej z metodą main() zaczniemy od utworzenia obiektów abstrakcyjnej klasy KebabBar, które odwołają się do konkretnych kebabiarni, następnie realizacja zamówienia w każdej z nich będzie przypisywana do obiektu odpowiadającego za produkt.
public class KebabTestDrive {
public static void main(String[] args) {
KebabBar kebabUZdzicha = new KebabUZdzichaBar();
KebabBar januszexKebab = new JanuszexKebabBar();
Kebab kebab = kebabUZdzicha.orderKebab("baranina");
System.out.println("Marian zamówił " + kebab);
kebab = kebabUZdzicha.orderKebab("kurczak");
System.out.println("Stefan zamówił " + kebab);
kebab = kebabUZdzicha.orderKebab("wegański");
System.out.println("Brian zamówił " + kebab);
kebab = januszexKebab.orderKebab("baranina");
System.out.println("Grażyna zamówiła " + kebab);
kebab = januszexKebab.orderKebab("kurczak");
System.out.println("Halina zamówiła " + kebab);
kebab = januszexKebab.orderKebab("wegański");
System.out.println("Jessica zamówiła " + kebab);
}
}
Oto, co „wypluła” konsola:
--- Przyrządzanie kebaba: Kebab w bułce z baraniną ---
Przygotowywanie: Kebab w bułce z baraniną
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Marian zamówił ---- Kebab w bułce z baraniną----
Ciasto grube — bułka
Pokrojone mięso z baraniny
Sos ostry
Ogórek, Cebula, Pomidor, Sałata
--- Przyrządzanie kebaba: Kebab w bułce z kurczakiem ---
Przygotowywanie Kebab w bułce z kurczakiem
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Stefan zamówił ---- Kebab w bułce z kurczakiem----
Ciasto grube — bułka
Pokrojone mięso z kurczaka
Sos ostry
Ogórek, Cebula, Pomidor, Sałata
--- Przyrządzanie kebaba: Wegański kebab w bułce ---
Przygotowywanie: Wegański kebab w bułce
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Brian zamówił ---- Wegański kebab w bułce----
Ciasto grube — bułka
Kotlety sojowe
Sos ostry
Ogórek, Cebula, Pomidor, Sałata
--- Przyrządzanie kebaba: Kebab w picie z baraniną ---
Przygotowywanie: Kebab w picie z baraniną
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Grażyna zamówiła ---- Kebab w picie z baraniną----
Ciasto cienkie — pita
Pokrojone mięso z baraniny
Sos mieszany
Pomidor, Ogórek, Sałata
--- Przyrządzanie kebaba: Kebab w picie z kurczakiem ---
Przygotowywanie Kebab w picie z kurczakiem
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Halina zamówiła ---- Kebab w picie z kurczakiem----
Ciasto cienkie — pita
Pokrojone mięso z kurczaka
Sos mieszany
Pomidor, Ogórek, Sałata
--- Przyrządzanie kebaba: Wegański kebab w picie ---
Przygotowywanie: Wegański kebab w picie
Dodawanie składników do ciasta
Podpiekanie kebaba w opiekaczu (2-3 min.)
Zapakowanie kebaba do opakowania
Jessica zamówiła ---- Wegański kebab w picie----
Ciasto cienkie — pita
Kotlety sojowe
Sos mieszany
Pomidor, Ogórek, Sałata
Process finished with exit code 0
Jak widać, bardzo, ale to bardzo mocno uprościłem proces tworzenia kebaba — chciałem, żeby przykład był możliwie jak najprostszy, dlatego uprzejmie proszę się nie czepiać, że moje kebaby znacznie odbiegają od tych z rzeczywistości.
Fabryka Abstrakcyjna a Metoda Fabrykująca
Oba wzorce są do siebie w pewnym sensie podobne, gdyż hermetyzują proces tworzenia obiektów, umożliwiając dzięki temu zastosowanie luźnych powiązań kodu aplikacji, usunięcie sprzężeń i zmniejszenie zależności od konkretnych implementacji.
Jakie są dokładne różnice między nimi przedstawiam w poniższej tabeli.
Metoda Fabrykująca | Fabryka Abstrakcyjna |
---|---|
Wykorzystywana do oddzielania kodu klienta od klas rzeczywistych, których obiekty należy tworzyć, lub gdy z góry nie wiadomo, z jakich klas rzeczywistych przyjdzie nam korzystać. Aby zastosować takie rozwiązania, wystarczy utworzyć klasy podrzędne i zaimplementować metodę fabrykującą | Wykorzystywana zawsze wtedy, kiedy zaistnieje potrzeba wytwarzania rodziny produktów i chce się mieć pewność, że poszczególni klienci wytwarzają spójne rodziny produktów. |
Wykorzystuje mechanizm dziedziczenia do tworzenia obiektów | Wykorzystuje kompozycję obiektów do tworzenia ich |
Tworzeniem obiektów zajmują się odpowiednie klasy podrzędne. W ten sposób klient musi tylko wiedzieć, jakich typów abstrakcyjnych używa, a typami rzeczywistymi zajmują się odpowiednie podklasy | Udostępnia typ abstrakcyjny umożliwiający tworzenie rodziny produktów. Aby skorzystać z fabryki, należy utworzyć obiekt, który ją reprezentuje, a następnie przekazać go do odpowiedniego kodu, który wykorzystuje elementy typu abstrakcyjnego. |
Produkuje tylko jeden produkt, więc nie potrzebuje dużych interfejsów. Jedna metoda jest w tym przypadku wystarczająca | Po dodaniu nowych produktów interfejs musi zostać rozbudowany. Konieczność zmiany interfejsu oznacza konieczność dokonania takiej samej zmiany interfejsu w każdej klasie podrzędnej |
Implementuje odpowiedni kod w konstruktorze klasy abstrakcyjnej, który wykorzystuje typy rzeczywiste tworzone przez klasy podrzędne | Wykorzystuje metody fabrykujące do implementacji fabryk rzeczywistych. Fabryki rzeczywiste często implementują metody fabrykujące do tworzenia swoich produktów. W tym przypadku są wykorzystywane tylko i wyłącznie do wytwarzania produktów |
Zastosowania wzorca Fabryka Abstrakcyjna
Fabrykę abstrakcyjną stosujemy, gdy nasz kod powinien działać na produktach z różnych rodzin i jednocześnie zależy nam na uniknięciu ścisłej zależności od rzeczywistych klas produktów, które mogą nie być znane na wcześniejszym etapie bądź planujemy w przyszłości rozszerzyć naszą aplikację.
Wzorzec ten możemy także wykorzystać, gdy mamy do czynienia z klasą posiadającą zestaw metod fabrykujących (wytwórczych), które zbytnio przyćmiewają główną odpowiedzialność tej klasy.
Kolejne zastosowanie to sytuacja, w której klasa ma do czynienia z wieloma typami produktów. Wówczas można zebrać jej metody wytwórcze, umieszczając w osobnej klasie fabrycznej lub nawet w pełni zaimplementować Fabrykę Abstrakcyjną z ich pomocą.
Wady i zalety wzorca Fabryka Abstrakcyjna
Fabryka Abstrakcyjna, jak każde rozwiązanie jakiegokolwiek problemu, oprócz zalet ma również wady.
Zalety | Wady |
---|---|
Pewność, że otrzymywane produkty są ze sobą kompatybilne | Kod może być zbyt skomplikowany ze względu na konieczność wprowadzenia wielu interfejsów i klas podczas wdrażania tego wzorca |
Zapobieganie ścisłej zależności produktów rzeczywistych z kodem klienta | |
Przestrzeganie zasady pojedynczej odpowiedzialności — można zebrać kod tworzący produkty w jednym miejscu, dzięki czemu późniejsze utrzymanie kodu będzie zdecydowanie łatwiejsze | |
Przestrzeganie zasady otwarte/zamknięte — można wspierać nowe warianty produktów bez zbytniej ingerencji w kod klienta |
Kod źródłowy do pobrania
Jeśli chcesz pobrać kod programu z przykładem użycia opisanego wzorca i przetestować go na swoim komputerze, kliknij na poniższy przycisk:
- https://refactoring.guru/pl/design-patterns/abstract-factory
- Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra, Rusz głową! Wzorce Projektowe, wyd. HELION 2017 — rozdział 4.
0 komentarzy