Fabryka Abstrakcyjna bardziej praktyczny przykład
Intencją wzorca fabryki abstrakcyjnej jest zapewnienie interfejsu do tworzenia rodzin powiązanych lub zależnych obiektów bez określania ich konkretnych klas.
Wyjaśnijmy to teraz na bardziej praktycznym przykładzie
Zrobimy to, pracując z podobnym kontekstem, jak przy Metodzie Fabrycznej bardziej praktyczny przykład, czyli w kontekście aplikacji koszyka na zakupy, gdzie wyliczaliśmy koszty wysyłki. Możemy teraz rozszerzyć ten przykład, aby uzyskać dobry przypadek użycia dla wzorca fabryki abstrakcyjnej.
W przykładzie dla metody fabrycznej stwierdziliśmy, że każdy z Polski ma koszt wysyłki równy 10 z Anglii 20, a wszyscy inni koszt wysyłki równy 50. Jest to coś, co zwykle stosuje się w scenariuszu z koszykiem na zakupy, ale jeśli chcemy, aby ten scenariusz był nieco bliższy życiu, potrzebujemy czegoś więcej niż tylko kosztu wysyłki zależnego od kraju. Moglibyśmy chcieć dodać jakąś ofertę specjalną w zależności od kraju. Oczywiście moglibyśmy napisać kod, który tworzy te klasy i oblicza koszt wysyłki i ofertę specjalną np. w ten sposób:
var polandShippingCostsService = new PolandShippingCostsService();
var shipingCosts = polandShippingCostsService.ShippingCosts;
var polandSpecialOfferService = new PolandSpecialOfferService();
var specialOffer = polandSpecialOfferService.DiscountPercentage;
Tylko wtedy napotkamy podobne problemy, jak przed wdrożeniem wzorca metody fabrycznej, kod stwarza ścisłe sprzężenie między klasą klienta, która je tworzy, a różnymi implementacjami usług. Co więcej, robiąc to w ten sposób, nie przekazujemy intencji, że te przedmioty należą do siebie.
I tutaj możemy użyć fabryki abstrakcyjnej IshoppingCartFactory, która tworzy rodzinę powiązanych obiektów.
IshoppingCartFactory jest to interfejs, który zawiera metodę CreateShippingCostsService() do tworzenia IShippingCostsService i metodę CreateSpecialOfferService() do tworzenia IspecialOfferService. Jest to rodzina usług potrzebnych do dokonania zakupu. Oznacza to również, że musimy zdefiniować te interfejsy.
IShippingCostsService, który oblicza koszty wysyłki ShippingCosts i ISpecialOfferService, który oblicza ofertę specjalną jak procent zniżki DiscountPercentage. Zauważ, że tym razem pracujemy z interfejsami, a nie abstrakcyjnymi klasami bazowymi. Jeśli wolisz abstrakcyjną klasę bazową, oczywiście możesz jej użyć. Z doświadczenia wynika, że częściej używane są interfejsy. Jeśli potrzebujesz zapewnić podstawową funkcjonalność, którą potencjalnie można nadpisać, wybierz klasę abstrakcyjną, w przeciwnym razie wybierz interfejs.
W każdym razie konkretnymi implementacjami może być PolandShoppingCartFactory, która jest powiązana z PolandShippingCostsService i PolandSpecialOfferService.
Odpowiedzialność za tworzenie tych konkretnych implementacji usług spoczywa na PolandShoppingCartFactory za pomocą metod CreateShippingCostsService() i CreateSpecialOfferService(). Zauważ, że nadal zwracają one interfejsy zamiast konkretnych implementacji. Inną implementacją może być wtedy EnglandShoppingCartFactory.
EnglandShoppingCartFactory jest powiązana z klasami EnglandShippingCostsService i EnglandSpecialOfferService. Zgodnie z tą samą logiką jak dla Polski.
Klienta ShoppingCart wykorzystuje jedną z fabryk do tworzenia dwóch potrzebnych usług.
ShoppingCart współpracuje z interfejsem IShoppingCartFactory, a nie implementacją. Pozwala to na pracę z dowolną konkretną realizacją czy to dla Polski, czy dla Anglii, czy jakąś inną. Typowym sposobem zapewnienia implementacji fabryki jest użycie konstruktora. Do klienta możemy dodać metodę CalculateCosts(), która oblicza rzeczywiste koszty zakupu przy użyciu dwóch konkretnych usług.
Klienta pracuje na interfejsie przy użyciu jednej z tych fabryk, a dodanie nowej fabryki, takiej jak ItalyShoppingCartFactory, można zrobić bez konieczności zmiany klienta, ponieważ umowy pozostają takie same, a klient nie musi wiedzieć o konkretach wdrożenia. I to jest jeden z głównych powodów używania tego wzorca. Klient jest oddzielony od konkretnej realizacji. Przypiszmy to teraz do struktury wzorca Fabryki Abstrakcyjnej.
Struktura wzorca Fabryki Abstrakcyjnej
Kiedy mapujemy nasz przykład na strukturę wzorca, zaczynamy od produktu. Może to być abstrakcyjna klasa bazowa lub interfejs. Deklarujemy tutaj interfejs dla typu obiektu produktu. W naszym przykładzie są to klasy, IShippingCostsService i ISpecialOfferService które są Produktami Abstrakcyjnymi.
Następnie mamy konkretny produkt. Definiujemy produkt, który ma zostać stworzony przez odpowiednią fabrykę i implementuje abstrakcyjny interfejs produktu. W naszym przykładzie są to implementacje IShippingCostsService, czyli PolandShippingCostsService, oraz ISpecialOfferService, czyli PolandSpecialOfferService. Zauważ, że zapisujemy je jako należące do tej samej rodziny produktów, Rodzina 1. Gdybyśmy mieli pracować z inną rodziną produktów, na przykład z tymi związanymi z Anglią, jak analizowaliśmy wcześniej, zostałyby one odwzorowane na konkretne produkty z Rodziny 2.
Następna jest fabryka abstrakcyjna, która użycza swojej nazwy od wzorca i deklaruje interfejs dla operacji tworzących abstrakcyjne obiekty produktu. Innymi słowy, jest to interfejs IShoppingCartFactory z naszego przykładu. Metody na nim to CreateProductA i CreateProductB, które zwracają odpowiednie produkty abstrakcyjne. Realizacje tej abstrakcyjnej fabryki nazywane są konkretnymi fabrykami. Odpowiadają za efektywne tworzenie powiązanych konkretnych produktów.
Zauważ, że te metody tworzą konkretne produkty, ale zwracają je jako abstrakcyjne produkty, co jest możliwe, ponieważ konkretny produkt implementuje abstrakcyjny interfejs produktu. Zmapowany do naszego przypadku użycia, PolandShoppingCartFactory mapujemy do ConcreteFactory1, a EnglandShoppingCartFactory z naszego przykładu mapujemy do ConcreteFactory2.
I wreszcie klient. To część naszej aplikacji, która wymaga abstrakcyjnej implementacji produktu. Ważne jest to, że powinien używać tylko abstrakcyjnych implementacji produktu i fabryki, a nie konkretnej implementacji, aby oddzielić je od nich. Używa więc tylko interfejsów, a to jest koszyk z naszego przykładu. I w ten sposób w pełni zmapowaliśmy nasz przykład do struktury wzorca. Zaimplementujmy teraz to w kodzie.
Przykład implementacji
Najpierw dodajmy nasze abstrakcyjne produkty. Zaczynamy od interfejsu koszt dostawy IShippingCostsService, który zwraca koszt dostawy.
/// <summary>
/// Product
/// </summary>
public interface IShippingCostsService
{
decimal ShippingCosts { get; }
}
Następnie usługa oferta promocyjna IspecialOfferService, która zwraca procent rabatu.
/// <summary>
/// Product
/// </summary>
public interface ISpecialOfferService
{
int DiscountPercentage { get; }
}
Jak widzisz, są to różne interfejsy. Teraz zaimplementujmy je. Implementujemy koszt dostawy IShippingCostsService, dla konkretnego produktu. PolandShippingCostsService, który ustawiakoszt dostawyShippingCosts na 10.
/// <summary>
/// ConcreteProduct
/// </summary>
public class PolandShippingCostsService : IShippingCostsService
{
public decimal ShippingCosts => 10;
}
Następnie dla EnglandShippingCostsService, który ustawia koszt dostawy ShippingCosts na 20.
/// <summary>
/// ConcreteProduct
/// </summary>
public class EnglandShippingCostsService : IShippingCostsService
{
public decimal ShippingCosts => 20;
}
Następnie dla ItalyShippingCostsService, który ustawia koszt dostawy ShippingCosts na 50.
/// <summary>
/// ConcreteProduct
/// </summary>
public class ItalyShippingCostsService : IShippingCostsService
{
public decimal ShippingCosts => 50;
}
Jak widać, zaczynamy tutaj tworzyć rodziny. PolandShippingCostsService będzie należeć do jednej rodziny, a EnglandShippingCostsService do drugiej rodziny, a ItalyShippingCostsService do trzeciej rodziny i tak możemy dodawać kolejne.
Tworzymy również konkretne produkty dla naszego abstrakcyjnego produktu ISpecialOfferService. Dla PolandSpecialOffertService gdzie ustawiamyprocent zniżkiDiscountPercentage na 20.
/// <summary>
/// ConcreteProduct
/// </summary>
public class PolandSpecialOfferService : ISpecialOfferService
{
public int DiscountPercentage => 20;
}
Następnie dla EnglandSpecialOfferService ustawiamyprocent zniżkiDiscountPercentage na 15.
/// <summary>
/// ConcreteProduct
/// </summary>
public class EnglandSpecialOfferService : ISpecialOfferService
{
public int DiscountPercentage => 15;
}
Następnie dla ItalySpecialOfferService ustawiamyprocent zniżkiDiscountPercentage na 10.
/// <summary>
/// ConcreteProduct
/// </summary>
public class ItalySpecialOfferService : ISpecialOfferService
{
public int DiscountPercentage => 10;
}
Teraz dodajmy naszą fabrykę abstrakcyjną. W naszym przypadku jest to interfejs IShoppingCartFactory.
/// <summary>
/// Factory
/// </summary>
public interface IShoppingCartFactory
{
IShippingCostsService CreateShippingCostsService();
ISpecialOfferService CreateSpecialOfferService();
}
Ten interfejs opisuje sposób tworzenia rodziny powiązanych produktów abstrakcyjnych. W naszym przypadku użycia udostępnia dwie metody, CreateShippingCostsService(), która zwraca interfejs IshippingCostsService oraz CreateSpecialOfferService() zwracający interfejs ISpecialOfferService. Zauważ, że nie pracujemy tutaj z żadnymi konkretnymi implementacjami.
Następnie możemy rozpocząć wdrażanie naszych fabryk. Jak pamiętamy, potrzebujemy jednej fabryki na rodzinę. Tak więc pierwsza implementacja IShoppingCartFactory to PolandShoppingCartFactory.
/// <summary>
/// ConcreteFactory
/// </summary>
public class PolandShoppingCartFactory : IShoppingCartFactory
{
public IShippingCostsService CreateShippingCostsService()
{
return new PolandShippingCostsService();
}
public ISpecialOfferService CreateSpecialOfferService()
{
return new PolandSpecialOfferService();
}
}
Jest ona odpowiedzialny za tworzenie konkretnych wdrożeń PolandShippingCostsService i PolandSpecialOfferService. I to, co zwracają metody, to nadal interfejsy, z którymi pracujemy. Druga implementacja dla Anglii EnglandShoppingCartFactory. Jest analogiczna jak dla Polski.
/// <summary>
/// ConcreteFactory
/// </summary>
public class EnglandShoppingCartFactory : IShoppingCartFactory
{
public IShippingCostsService CreateShippingCostsService()
{
return new EnglandShippingCostsService();
}
public ISpecialOfferService CreateSpecialOfferService()
{
return new EnglandSpecialOfferService();
}
}
I trzecia implementacja dla Włoch ItalyShoppingCartFactory. Jest również analogiczna.
/// <summary>
/// ConcreteFactory
/// </summary>
public class ItalyShoppingCartFactory : IShoppingCartFactory
{
public IShippingCostsService CreateShippingCostsService()
{
return new ItalyShippingCostsService();
}
public ISpecialOfferService CreateSpecialOfferService()
{
return new ItalySpecialOfferService();
}
}
Następnie potrzebujemy klienta. Nazwijmy klienta zgodnie z przeznaczeniem koszyk na zakupy ShoppingCart.
/// <summary>
/// Client
/// </summary>
public class ShoppingCart
{
private readonly IShippingCostsService _shippingCostsService;
private readonly ISpecialOfferService _specialOfferService;
private int _orderCosts;
public ShoppingCart(IShoppingCartFactory factory)
{
_shippingCostsService = factory.CreateShippingCostsService();
_specialOfferService = factory.CreateSpecialOfferService();
_orderCosts = 500;
}
public void CalculateCosts()
{
Console.WriteLine($"Całkowity koszt zamówienia to: {_orderCosts - (_orderCosts / 100 * _specialOfferService.DiscountPercentage) + _shippingCostsService.ShippingCosts }");
}
}
Zaczynamy od dodania dwóch pól, to są nasze dwa abstrakcyjne produkty _shippingCostsService, _specialOfferService. Tworząc koszyk, chcemy przekazać, że należy użyć jednej określonej rodziny produktów. Dlatego usługę kosztów wysyłki i oferty promocyjnej tworzymy dla tej samej fabryki.
Dodajemy również pole do przechowywania całkowitych kosztów zamówienia _orderCosts i w celach testowych załóżmy, że łączny koszt wszystkich zamówionych przez nas pozycji to 500 PLN. Od tego zostanie odjęty procent oferty promocyjnej i dodany koszt wysyłki.
I dodajemy jeszcze jedną metodę, oblicz koszty CalculateCosts(), która ułatwi nam przetestowanie tego. W tej metodzie wypisujemy koszty zamówienia _orderCosts minus odjęty procent oferty promocyjnej, plus koszty wysyłki. I to wszystko. Napiszmy teraz kod, aby to przetestować.
Przejdźmy do pliku Program.cs. I stwórzmy nasze fabryki.
using AbstractFactory2;
Console.Title = "Abstract Factory";
var polandShoppingCartFactory = new PolandShoppingCartFactory();
var shoppingCartForPoland = new ShoppingCart(polandShoppingCartFactory);
shoppingCartForPoland.CalculateCosts();
var englandShoppingCartFactory = new EnglandShoppingCartFactory();
var shoppingCartForEngland = new ShoppingCart(englandShoppingCartFactory);
shoppingCartForEngland.CalculateCosts();
var italyShoppingCartFactory = new ItalyShoppingCartFactory();
var shoppingCartForItaly = new ShoppingCart(italyShoppingCartFactory);
shoppingCartForItaly.CalculateCosts();
Console.ReadKey();
Tworzymy pierwszą fabrykę dla Polski, a następnie przekazujemy fabrykę do koszyka i wywołujemy metodę oblicz koszty CalculateCosts na naszym koszyku, co spowoduje wypisanie rzeczywistych kosztów w oknie danych wyjściowych konsoli. To samo robimy Anglii i Włoch. I teraz możemy przetestować nasz kod i go uruchomić.
Wynik działania
Całkowity koszt zamówienia to: 410
Całkowity koszt zamówienia to: 445
Całkowity koszt zamówienia to: 500
Całkowite koszty są wypisywane na naszej konsoli i są różne, co oznacza, że różne fabryki, z których korzystaliśmy, skutkowały powstaniem różnych produktów i ich ostatecznym wykorzystaniem.
Zastosowanie
Możemy użyć fabryki abstrakcyjnej, gdy nasza aplikacja powinna być niezależna od sposobu tworzenia i reprezentowania jej produktów. Nasz przykład z życia dobrze to przedstawia.
Możemy użyć, gdy chcemy udostępnić bibliotekę klas produktów i chcemy tylko ujawnić ich interfejsy, a nie ich implementacje.
Możemy użyć, gdy system powinien być skonfigurowany z jedną z wielu rodzin produktów. Jak się dowiedzieliśmy, abstrakcyjna fabryka zajmuje się rodzinami produktów, produktami, które należą do siebie.
Możemy użyć, gdy rodzina powiązanych obiektów produktów ma być używana razem i chcemy wymusić to ograniczenie.