Metoda Wytwórcza cz.2

Metoda Wytwórcza bardziej praktyczny przykład

Jak już wiemy intencją wzorca, jest zdefiniowanie interfejsu do tworzenia obiektów, ale to podklasy decydują, którą klasę utworzyć. Innymi słowy, metoda fabryczna pozwala klasie odroczyć tworzenie instancji do podklas.

Wyjaśnijmy to teraz na bardziej praktycznym przykładzie

Wyobraź sobie, że tworzysz część systemu koszyka na zakupy. I w pewnym momencie chcesz wyliczyć koszty wysyłki. Jedna z usług wylicza koszty wysyłki w zależności od kraju, w którym znajduje się użytkownik. Inna wylicza koszty wysyłki w zależności od jakiegoś kodu rabatowego. Zatem obie usługi nie są skonstruowane w ten sam sposób, a logika obliczania rzeczywistego rabatu nie jest taka sama.

Jednak mają coś wspólnego, koszt wysyłki, który należy obliczyć. Moglibyśmy napisać kod, który tworzy te klasy i oblicza koszt wysyłki. np. w ten sposób:

var codeShippingCostsService = new CodeShippingCostsService(Guid.NewGuid());
var cost = codeShippingCostsService.ShippingCosts();
lub
var countryShippingCostsService = new CountryShippingCostsService("PL");
var cost = countryShippingCostsService.ShippingCosts();

Ten kod stwarza jednak ścisłe sprzężenie między klasą klienta, która je tworzy, a różnymi implementacjami usługi która wylicza koszt wysyłki . Wzorzec metody fabrycznej pomaga nam tego uniknąć. Robimy to, dodając klasę bazową lub interfejs o nazwie ShippingCostsService.

Jest to klasa, z którą może wchodzić w interakcje dowolna część aplikacji, która musi znać koszt wysyłki. Konkretna realizacja nie jest ważna dla klienta. Teraz klient, który jest częścią systemu koszyka zakupowego, musi znać koszt wysyłki, nie musi wiedzieć, którą podklasę musi utworzyć. Nie ma znaczenia, czy jest to koszt wysyłki na podstawie kodu, czy na podstawie kraju. Wszystko, co musi wiedzieć, to kiedy potrzebuje takiej instancji.

Ale jak to zrobić?

W tym celu tworzymy podstawową fabrykę kosztów wysyłki z jedną abstrakcyjnej metodą, CreateShippingCostsService. Nazywa się to metodą fabryczną. Konkretne implementacje tej fabryki to CodeShippingCostsFactory i CountryShippingCostsFactory.

Klient pracuje na klasie bazowej ShippingCostsFactory, więc używa dowolnej dostarczonej implementacji, aby uzyskać konkretną implementację usługi za pośrednictwem konkretnej implementacji ShippingCostsFactory z konkretną implementacją dla metody CreateShippingCostsService.

Za pomocą metody Factory hermetyzujemy logikę, aby stworzyć rzeczywistą podklasę, więc klient nie musi już o tym wiedzieć. A ponieważ pracujemy nad interfejsem zamiast nad konkretną implementacją, zapobiegamy ścisłemu połączeniu.

Przypiszmy to do struktury wzorca.

Struktura wzorca metody fabrycznej

Kiedy mapujemy to na strukturę wzorca, zaczynamy od produktu. Może to być abstrakcyjna klasa bazowa lub interfejs dla rzeczy, która ma zostać stworzona. W ten sposób definiujemy interfejs obiektów, które tworzy metoda fabryki. W naszym przypadku jest to klasa ShippingCostsService.

Następnie mamy konkretne produkty, implementacje interfejsu produktu. Jeśli zmapujemy to do naszego przykładu, będzie to CodeShippingCostsService i CountryShippingCostsService.

Creator następnie deklaruje metodę fabryki. Ta metoda fabryki musi zwracać obiekt typu produkt. W naszym przypadku jest to ShippingCostsFactory z abstrakcyjną metodą CreateShippingCostsService. Możemy zmapować to do metody fabrycznej. Sam creator może również wywołać metodę fabryki w celu utworzenia obiektu produktu.

I wreszcie mamy konkretnegoCreatora. Są to implementacje creatora, które nadpisują metodę fabryki, aby zwrócić instancję ConcreteProduct. Zmapowane do naszego przykładu są to CodeShippingCostsFactory i CountryShippingCostsFactory. I dzięki temu mamy naszą strukturę wzorca. Zaimplementujmy teraz to w kodzie.

Przykład implementacji

Zacznijmy od naszego produktu, ShippingCostsService

namespace FactoryMethod
{
    /// <summary>
    /// Product
    /// </summary>
    public abstract class ShippingCostsService
    {
        public abstract decimal ShippingCosts { get; }
        public override string ToString() => GetType().Name;
    }
}

Jest to klasa abstrakcyjna, ponieważ chcemy, aby służyła jako interfejs dla konkretnej implementacji. Na nim możemy dodać właściwości lub metody, takie jak ShippingCosts, z których mogą korzystać klienci. To również jest oznaczone słowem kluczowym abstract, ponieważ chcemy, aby konkretne produkty implementowały logikę, która się za tym kryje.

Zauważ, że podobnie jak w przypadku większości implementacji wzorców, które wymagają abstrakcyjnej klasy bazowej, możesz użyć interfejsu jako alternatywy. Zastępujemy również metodę ToString(), aby zwracała rzeczywistą nazwę typu. Dzięki temu łatwiej będzie zobaczyć, co się dzieje, kiedy będziemy testować nasz kod.

Następnie implementujmy to. Dodajemy CountryShippingCostsService, który implementuje naszą klasę abstrakcyjną ShippingCostsService.

To jest konkretny produkt

namespace FactoryMethod
{
    /// <summary>
    /// ConcreteProduct
    /// </summary>
    public class CountryShippingCostsService : ShippingCostsService
    {
        private readonly string _countryIdentifier;

        public CountryShippingCostsService(string countryIdentifier)
        {
            _countryIdentifier = countryIdentifier;
        }

        public override decimal ShippingCosts
        {
            get
            {
                switch (_countryIdentifier)
                {
                    case "PL":
                        return 10;
                    case "GB":
                        return 20;
                    default:
                        return 50;
                }
            }
        }
    }
}

W zależności od przekazanego identyfikatora kraju należy zwrócić inny koszt wysyłki. Tak więc przechowujemy identyfikator w polu tylko do odczytu. Nie chcemy, żeby to się zmieniło po zbudowaniu obiektu.

Następnie zastępujemy ShippingCosts, aby dodać naszą implementację tej właściwości. Powiedzmy, że w przypadku tego demo, gdy jesteśmy z Polski, otrzymujemy najmniejszy koszt wysyłki 10, w przypadku Anglii 20, a z pozostałych krajów 50.

Naszą kolejną implementacją jest CodeShippingCostsService

namespace FactoryMethod
{
    /// <summary>
    /// ConcreteProduct
    /// </summary>
    public class CodeShippingCostsService : ShippingCostsService
    {
        private readonly Guid _code;

        public CodeShippingCostsService(Guid code)
        {
            _code = code;
        }

        public override decimal ShippingCosts
        {
            // dla celów tego programu zwracamy poprostu
            get => 25;
        }
    }
}

Implementujemy w podobny sposób. Usługa ShippingCostsService jest zaimplementowana i zastępujemy właściwość ShippingCosts. Załóżmy, że każdy kod daje taki sam koszt wysyłki. W prawdziwym życiu tutaj możesz dodać bardziej rozbudowaną implementację. Nie będziemy tego tutaj wdrażać, ponieważ ważne jest dla nas tylko to, że mamy dwa różne produkty.

Teraz fabryki

namespace FactoryMethod
{
    /// <summary>
    /// Creator
    /// </summary>
    public abstract class ShippingCostsFactory
    {
        public abstract ShippingCostsService CreateShippingCostsService();
    }
}

Jak już wiemy, zaczynamy od abstrakcyjnej klasy ShippingCostsFactory z jedną abstrakcyjną metodą, CreateShippingCostsService, która zwraca ShippingCostsService, klasę bazową dla naszych konkretnych kosztów wysyłki. A potem musimy wdrożyć konkretne wdrożenia.

namespace FactoryMethod
{
    /// <summary>
    /// ConcreteCreator
    /// </summary>
    public class CountryShippingCostsFactory : ShippingCostsFactory
    {
        private readonly string _countryIdentifier;
        public CountryShippingCostsFactory(string countryIdentifier)
        {
            _countryIdentifier = countryIdentifier;
        }

        public override ShippingCostsService CreateShippingCostsService()
        {
            return new CountryShippingCostsService(_countryIdentifier);
        }
    }
}

CountryShippingCostsFactory publikuje informacje o usłudze CountryShippingCostsService, dla której potrzebuje identyfikatora kraju. Więc to ta fabryka jest odpowiedzialna za stworzenie usługi CountryShippingCostsService.

W podobny sposób nasz CodeShippingCostsFactory.

namespace FactoryMethod
{
    /// <summary>
    /// ConcreteCreator
    /// </summary>
    public class CodeShippingCostsFactory : ShippingCostsFactory
    {
        private readonly Guid _code;

        public CodeShippingCostsFactory(Guid code)
        {
            _code = code;
        }

        public override ShippingCostsService CreateShippingCostsService()
        {
            return new CodeShippingCostsService(_code);
        }
    }
}

CodeShippingCostsFactory odpowiada za tworzenie i zarządzanie czasem życia usługi CodeShippingCostsService, przekazując kod, który otrzymuje z konstruktora CodeShippingCostsFactory. I dzięki temu mamy wszystkie części naszego wzoru.

Możemy to przetestować.

Przejdźmy do pliku Program.cs. I stwórzmy nasze dwie fabryki.

using FactoryMethod;

Console.Title = "Factory Method";

var factories = new List<ShippingCostsFactory> {
    new CodeShippingCostsFactory(Guid.NewGuid()),
    new CountryShippingCostsFactory("PL") };

Przechowujemy je na liście, co ułatwia przeglądanie ich, aby zobaczyć, co się dzieje.
To dzięki tym fabrykom można tworzyć rzeczywiste usługi.
Więc to właśnie robimy dalej.

foreach (var factory in factories)
{
    var shippingCostsService = factory.CreateShippingCostsService();
    Console.WriteLine($"Koszt wysyłki to {shippingCostsService.ShippingCosts} zł. i pochodzi z {shippingCostsService}");
}

Console.ReadKey();

W pętli każda fabryka tworzy odpowiedni serwis do wyliczania kosztów dostawy. A następnie wypisujemy Koszt wysyłki i skąd pochodzi. W ten sposób możemy zobaczyć, co faktycznie zostało stworzone.

Należy tutaj pamiętać, że klient nie wie o żadnych konkretnych wdrożeniach. Fabryki, z którymi współpracuje, są abstrakcyjne, podobnie jak usługi do obliczania kosztu.

Wynik działania:

Koszt wysyłki to 25 zł. i pochodzi z CodeShippingCostsService
Koszt wysyłki to 10 zł. i pochodzi z CountryShippingCostsService

I widzimy, dwa produkty zostały stworzone przez nasze dwie fabryki bez wiedzy klienta o konkretnych wykonaniach.

Zastosowanie

Dobrymi przypadkami użycia tego wzorca są sytuacje, w których klasa nie może przewidzieć klasy obiektów, które musi utworzyć, innymi słowy, gdy nie wiemy z góry, z jakimi typami obiektów powinien współpracować Twój kod. W takim przypadku konkretni twórcy mogą wziąć na siebie tę odpowiedzialność.

Innym dobrym przypadkiem użycia jest sytuacja, gdy klasa chce, aby jej podklasy określały obiekty, które tworzy. Umożliwia to scenariusz, w którym standardowe komponenty lub klasy mogą być rozszerzane za pomocą podklas. Tutaj również oznacza to, że twórca bierze na siebie odpowiedzialność za tworzenie obiektów. Następnie, klasy delegują odpowiedzialność za tworzenie do jednej z kilku podklas pomocniczych.

Kod źródłowy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *