Fabryka abstrakcyjna (Abstract Factory)
Główna różnica między metodą fabryczną a fabryką abstrakcyjną polega na tym, że metoda fabryczna to pojedyncza metoda, a fabryka abstrakcyjna to obiekt. Metoda fabryczna jest tylko metodą, można ją przesłonić w podklasie, podczas gdy fabryka abstrakcyjna to obiekt, który ma wiele metod fabrycznych. Fabryka abstrakcyjna w przeciwieństwie do metody fabrycznej jest odpowiedzialna za tworzenie kilku różnych typów.
Cel
Fabryka abstrakcyjna jest kreacyjnym wzorcem projektowym, który udostępnia interfejs do tworzenia rodzin powiązanych lub zależnych obiektów bez określania ich konkretnych klas. Czyli pozwala tworzyć rodziny spokrewnionych ze sobą obiektów bez określania ich konkretnych klas.
Problem
Powiedzmy, że piszemy aplikację dla koncernu samochodowego Volkswagen Group. I obecnie nasza aplikacja składa się z następujących klas:
Sedan, Hatchback, Combi, Cabriolet…
Jest to rodzina spokrewnionych produktów dla konkretnej marki samochodu. I mogą być różne marki w ramach rodziny samochodów:
Volkswagen, Audi, Skoda, Porsche…
I teraz trzeba produkować poszczególne samochody:
Sedan, Hatchback, Combi, Cabriolet
dla każdej marki w ramach rodziny:
Volkswagen, Audi, Skoda, Porsche
Każda fabryka koncernu produkuje jedną rodzinę/markę samochodów. Sedan zaprojektowany i wyprodukowany w Audi nie pasuje do Kombi zaprojektowanego i wyprodukowanego w Skodzie, to są inne rodziny/marki samochodów.
Ponadto, nie chcemy zmieniać istniejącego kodu tylko po to, aby dodać nowy produkt lub rodzinę produktów do programu. Producenci samochodów dość często wypuszczają nowe katalogi i nie chciałbyś zmieniać głównej części kodu za każdym razem, gdy tak się stanie.
Rozwiązanie
Pierwszą rzeczą, jaką proponuje wzorzec projektowy Fabryka abstrakcyjna, jest wyraźne określenie interfejsów dla każdego konkretnego produktu z wybranej rodziny (np. Sedan, Hatchback, Combi, Cabriolet)
Następnie trzeba sprawić, aby wszystkie warianty produktów były zgodne z tymi interfejsami. Na przykład wszystkie Sedany implementują interfejs Sedan, wszystkie Hatchbacki implementują interfejs Hatchback, wszystkie Combi implementują interfejs Combi i tak dalej.
Kolejnym krokiem jest deklaracja interfejsu Fabryka abstrakcyjna, który zawrze listę metod kreacyjnych wszystkich produktów w ramach jednej rodziny na przykład:
CreateCabriolet(), CreateCombi(), CreateHatchback(), CreateSedan().
Metody te muszą zwracać wyłącznie abstrakcyjne typy produktów, reprezentowane uprzednio określonymi interfejsami: ISedan, IHatchback, ICombi, ICabriolet.
Każda konkretna fabryka odpowiada konkretnemu wariantowi produktu (konkretnej marce produkowanych samochodów). A co z poszczególnymi wariantami produktów?
Dla każdego wariantu rodziny produktów tworzymy osobną klasę fabryczną na podstawie interfejsu FabrykaAbstrakcyjna. Klasa fabryczna to taka klasa, która zwraca produkty danego rodzaju. FabrykaAudi może zwracać wyłącznie obiekty: AudiSedan, AudiHatchback, AudiCombi oraz AudiCabriolet.
Kod kliencki będzie korzystał z fabryk oraz produktów za pośrednictwem ich interfejsów abstrakcyjnych. Dzięki temu będzie można zmienić typ fabryki przekazywanej kodowi klienckiemu oraz zmienić wariant produktu, jaki otrzyma kod kliencki i to wszystko bez ryzyka popsucia samego kodu klienckiego. Klienta nie powinno obchodzić to, z jaką konkretnie klasą fabryczną ma do czynienia.
Załóżmy, że klient potrzebuje fabrykę do stworzenia Sedana. Klient nie powinien być świadom klasy tej fabryki ani martwić się o rodzaj sedana, z jakim przyjdzie mu pracować. Czy będzie to Sedan Audi, czy też Skody, klient powinien traktować wszystkie w taki sam sposób, za pośrednictwem interfejsu abstrakcyjnego ISedan.
Dzięki temu podejściu klient wie tylko tyle, że sedan implementują jakąś metodę FunctionWithSedan(). Ponadto, niezależnie od wariantu zwracanego sedana, zawsze będą one pasowały do Hatchbacka, Combi lub Cabrioleta jakie produkuje dany obiekt fabryczny.
Pozostaje do wyjaśnienia jeszcze jedna sprawa: jeśli klient ma do czynienia wyłącznie z interfejsami abstrakcyjnymi, to kto właściwie tworzy rzeczywiste obiekty fabryczne?
Na ogół aplikacja tworzy konkretny obiekt fabryczny na etapie inicjalizacji. Tuż przed tym wybiera stosowny typ fabryki zależnie na przykład od konfiguracji lub środowiska.
Przykłady użycia w kodzie
Fabryka abstrakcyjna definiuje interfejs służący tworzeniu poszczególnych produktów, ale pozostawia faktyczne tworzenie produktów konkretnym klasom fabrycznym. Każdy typ fabryki odpowiada jednemu z wariantów produktu.
Identyfikacja
Ten wzorzec można łatwo rozpoznać na podstawie metod zwracających obiekt fabryczny, który potem służy tworzeniu konkretnych pod komponentów.
Przykład implementacji
Produkt IAbstractHatchback
Każdy produkt z rodziny produktów powinien mieć interfejs bazowy. Wszystkie warianty produktu muszą implementować ten interfejs. Właściwa interakcja jest możliwa tylko między produktami tego samego wariantu.
public interface IAbstractHatchback
{
string FunctionWithHatchback();
}
Produkty są tworzone przez odpowiednie Fabryki.
class AudiHatchback : IAbstractHatchback
{
public string FunctionWithHatchback()
{
return "Metoda zwraca AudiHatchback";
}
}
class SkodaHatchback : IAbstractHatchback
{
public string FunctionWithHatchback()
{
return "Metoda zwraca SkodaHatchback";
}
}
Produkt IAbstractSedan
Produkt IAbstractSedan jest w stanie zrobić swoje… ale może też współpracować z IAbstractCombi. Fabryka Abstrakcyjna dba o to, aby wszystkie tworzone przez nią produkty były tego samego wariantu, a co za tym idzie, kompatybilne.
public interface IAbstractSedan
{
string FunctionWithSedan();
string AnotherFunctionWithSedan(IAbstractCombi collaborator);
}
Produkty są tworzone przez odpowiednie Fabryki. Wariant AudiSedan może działać poprawnie z wariantem AudiCombi, gdyż przyjmuje wystąpienie IAbstractCombi jako argument.
class AudiSedan : IAbstractSedan
{
public string FunctionWithSedan()
{
return "Metoda zwraca AudiSedan";
}
public string AnotherFunctionWithSedan(IAbstractCombi collaborator)
{
var result = collaborator.FunctionWithCombi();
return $"Wynik współpracy AudiSedan z ({result})";
}
}
class SkodaSedan : IAbstractSedan
{
public string FunctionWithSedan()
{
return "Metoda zwraca SkodaSedan";
}
public string AnotherFunctionWithSedan(IAbstractCombi collaborator)
{
var result = collaborator.FunctionWithCombi();
return $"Wynik współpracy SkodaSedan z ({result})";
}
}
Produkt IAbstractCombi
public interface IAbstractCombi
{
string FunctionWithCombi();
}
Produkty są tworzone przez odpowiednie Fabryki.
class AudiCombi : IAbstractCombi
{
public string FunctionWithCombi()
{
return "Metoda zwraca AudiCombi";
}
}
class SkodaCombi : IAbstractCombi
{
public string FunctionWithCombi()
{
return "Metoda zwraca SkodaCombi";
}
}
Produkty IAbstractCabriolet
public interface IAbstractCabriolet
{
string FunctionWithCabriolet();
}
Produkty są tworzone przez odpowiednie Fabryki.
class AudiCabriolet : IAbstractCabriolet
{
public string FunctionWithCabriolet()
{
return "Metoda zwraca AudiCabriolet";
}
}
class SkodaCabriolet : IAbstractCabriolet
{
public string FunctionWithCabriolet()
{
return "Metoda zwraca SkodaCabriolet";
}
}
Fabryka IAbstractFactory
Interfejs Fabryka abstrakcyjna deklaruje zestaw metod, które zwracają różne produkty abstrakcyjne. Produkty te nazywane są rodziną. Produkty jednej rodziny zazwyczaj potrafią ze sobą współpracować.
public interface IAbstractFactory
{
IAbstractCabriolet CreateCabriolet();
IAbstractCombi CreateCombi();
IAbstractHatchback CreateHatchback();
IAbstractSedan CreateSedan();
}
Konkretne Fabryki produkują rodzinę produktów, które należą do danego wariantu. Fabryka gwarantuje, że powstałe produkty są kompatybilne. Zwróć uwagę, że sygnatury metod Fabryki zwracają abstrakcyjny produkt, podczas gdy wewnątrz metody powstaje konkretny produkt.
AudiFactory
class AudiFactory : IAbstractFactory
{
public IAbstractCabriolet CreateCabriolet()
{
return new AudiCabriolet();
}
public IAbstractCombi CreateCombi()
{
return new AudiCombi();
}
public IAbstractHatchback CreateHatchback()
{
return new AudiHatchback();
}
public IAbstractSedan CreateSedan()
{
return new AudiSedan();
}
}
SkodaFactory
class SkodaFactory : IAbstractFactory
{
public IAbstractCabriolet CreateCabriolet()
{
return new SkodaCabriolet();
}
public IAbstractCombi CreateCombi()
{
return new SkodaCombi();
}
public IAbstractHatchback CreateHatchback()
{
return new SkodaHatchback();
}
public IAbstractSedan CreateSedan()
{
return new SkodaSedan();
}
}
Kod klienta
Kod klienta wywołuje metody kreacyjne obiektu fabrycznego, zamiast tworzyć produkty bezpośrednio, wywołując konstruktor (za pomocą operatora new). Skoro dana fabryka odpowiada jednemu z wariantów produktu, to wszystkie jej produkty będą ze sobą kompatybilne.
Kod klienta współpracuje z fabrykami i produktami wyłącznie poprzez ich abstrakcyjne interfejsy. Dzięki temu jeden klient jest kompatybilny z wieloma różnymi produktami. Wystarczy stworzyć nową konkretną klasę fabryczną i przekazać ją kodowi klienta.
Kod klienta współpracuje z fabrykami i produktami tylko poprzez typy abstrakcyjne. Pozwala to przekazać dowolną podklasę fabryki lub produktu do kodu klienta bez jego łamania.
class Client
{
public void Main()
{
Console.WriteLine("Klient: Testowanie kodu klienta z pierwszym typem fabryki:");
ClientMethod(new AudiFactory());
Console.WriteLine();
Console.WriteLine("Klient: Testowanie tego samego kodu klienta z drugim typem fabryki:");
ClientMethod(new SkodaFactory());
Console.WriteLine();
}
public void ClientMethod(IAbstractFactory factory)
{
IAbstractCabriolet cabriolet = factory.CreateCabriolet();
IAbstractCombi combi = factory.CreateCombi();
IAbstractHatchback hatchback = factory.CreateHatchback();
IAbstractSedan sedan = factory.CreateSedan();
Console.WriteLine(cabriolet.FunctionWithCabriolet());
Console.WriteLine(combi.FunctionWithCombi());
Console.WriteLine(hatchback.FunctionWithHatchback());
Console.WriteLine(sedan.FunctionWithSedan());
Console.WriteLine(sedan.AnotherFunctionWithSedan(combi));
}
}
I wywwołanie klinta w kasie Program
new Client().Main();
Wynik działania
Klient: Testowanie kodu klienta z pierwszym typem fabryki: Metoda zwraca AudiCabriolet Metoda zwraca AudiCombi Metoda zwraca AudiHatchback Metoda zwraca AudiSedan Wynik współpracy AudiSedan z (Metoda zwraca AudiCombi) Klient: Testowanie tego samego kodu klienta z drugim typem fabryki: Metoda zwraca SkodaCabriolet Metoda zwraca SkodaCombi Metoda zwraca SkodaHatchback Metoda zwraca SkodaSedan Wynik współpracy SkodaSedan z (Metoda zwraca SkodaCombi)
Zastosowanie
W dobrze zaprojektowanej aplikacji każda klasa ma jedną odpowiedzialność (Zasada pojedynczej odpowiedzialności – SOLID). Gdy zaś klasa ma do czynienia z wieloma typami produktów, powinniśmy zebrać jej metody wytwórcze i umieścić je w osobnej klasie fabrycznej.
Fabrykę abstrakcyjną, stosujemy, gdy chcemy, aby nasz kod działał na produktach z różnych rodzin, a jednocześnie nie chcemy, aby ściśle zależał od konkretnych klas produktów. Mogą one bowiem być nieznane na wczesnym etapie tworzenia aplikacji albo chcemy umożliwić naszej aplikacji przyszłą rozszerzalność.
Fabryka abstrakcyjna dostarcza interfejs służący tworzeniu obiektów z różnych klas danej rodziny produktów.
Zalety i wady
- Fabryka abstrakcyjna jest szczególnie przydatny, gdy klient nie wie dokładnie, jaki typ stworzyć.
- Klienci manipulują instancjami poprzez swoje abstrakcyjne interfejsy.
- Fabryka abstrakcyjna pomaga kontrolować klasy obiektów tworzonych przez aplikację. Ponieważ fabryka zawiera w sobie odpowiedzialność za tworzenia obiektów, izoluje klientów od klas implementacyjnych.
- Zasada pojedynczej odpowiedzialności – możemy mieć kod kreacyjny produkty w jednym miejscu w programie, ułatwiając tym samym późniejsze utrzymanie kodu.
- Zasada otwarte/zamknięte – możemy wprowadzać nowe warianty produktów bez psucia istniejącego kodu klienckiego.
- Zapobiegamy ścisłemu sprzęgnięciu produktów z kodem klienckim.
- Gdy stosujemy, wzorzec fabryki abstrakcyjnej mamy pewność, że wszystkie produkty są ze sobą kompatybilne.
- Obsługa nowych rodzajów produktów wymaga rozszerzenia interfejsu fabryki, co wiąże się ze zmianą klasy AbstractFactory i wszystkich jej podklas.
- Nasz kod może stać się bardziej skomplikowany, niż powinien. Wynika to z konieczności dodania nowych interfejsów i klas.