W tym artykule postaramy się zrozumieć podstawy wzorca fabryki (Factory Pattern), poznamy jakie są korzyści użycia tego wzorca oraz jak może zostać zaimplementowany w języku C#.
W tym artykule postaramy się zrozumieć podstawy wzorca fabryki (Factory Pattern), poznamy jakie są korzyści użycia tego wzorca oraz jak może zostać zaimplementowany w języku C#.
Jest praktycznie niemożliwe, aby utworzyć aplikację składającą się tylko z dwóch klas. Zazwyczaj aplikacja składa się z wielu
klas, każda klasa jest odpowiedzialna za żądaną funkcjonalność. Oznacza to, że jest praktycznie niemożliwe, żeby klasy nie
komunikowały się ze sobą. Może to być osiągnięte w bardzo prosty sposób, jeżeli pozwolimy klasie na utworzenie instancji klasy
której potrzebujemy, będziemy w stanie wywoływać potrzebne metody.
Załóżmy, że mamy klasę A, która ma wywołać metodę z klasy B.
Wystarczy, że obiekt klasy B będzie umieszczony wewnątrz A.
Będziemy wówczas mogli wywołać metodę, której potrzebujemy. Dla łatwiejszej interpretacji problemu proszę spojrzeć na
poniższy kod:
// Definicja klasy A public class A { // wewnątrz klasy A mamy obiekt B private B b; // w kontstruktorze A tworzymy obiekt klasy B public A() { b = new B(); } // Metoda klasy korzysta z obiektu klasy B i wywołuje metodę z tej klasy public void EndTheIssue() { b.DoTaskOne(); } } // Definicja klasy B public class B { // Wykonanie zadanie public void DoTaskOne() { Console.WriteLine("Zakończ zadanie"); } }Podejście zapreznetowane wyżej będzie działać ale ma pewne wady. Pierwszym z nich jest fakt, że klasa taka musi wiedzieć o każdej klasie, którą chce wykorzystać. Spowoduje to, że zarządzanie taką aplikacją będzie naprawdę trudne. Ponadto takie podejście powoduje znaczy wzrost połączeń pomiędzy klasami.
// interfejs to tylko definicja interface IDoTask { void DoTaskOne(); } // Definicja klasy B, która implementuje metodę interfejsu public class B : IDoTask { // Wykonanie zadanie public void DoTaskOne() { Console.WriteLine("Zakończ zadanie"); } } public class A { private IDoTask task; public A() { // jak utworzyć nowy obiekt w tym miejscu? // wywołanie task = new B(); // wydaje się niewłaściwe } // Metoda klasy korzysta z interfejsu i wywołuje...o tym za chwilę public void EndTheIssue() { task.DoTaskOne(); } }Powyższy przykład pokazuje klasy, które zostały zaprojektowane w bardzo dobry sposób. Moduły wyższego poziomu zależą od abstrakcji a moduły niższego poziomu implementacją tą abstrakcję. Ale, ale…jak zamierzamy utworzyć obiekt klasy B? Powinniśmy zrobić jak w poprzednim przykładzie, tj. utworzyć nowy obiekt B w konstruktorze klasy A? Czy jednak nie wpłynęłoby to na utracenie zachowania zasady odwracania zależności?
// interfejs to tylko definicja interface IDoTask { void DoTaskOne(); } public class FactoryPatern { // metoda zwracająca konkretne wykonanie // w naszym przypadku chodzi o obiekt klasy B public B GetConcreteDoable() { return new B(); } } // Definicja klasy B, która implementuje metodę interfejsu public class B : IDoTask { // Wykonanie zadanie public void DoTaskOne() { Console.WriteLine("Zakończ zadanie"); } } public class A { private IDoTask task; public A() { // tworzymy nowy obiekt klasy FactoryPatern FactoryPatern fp = new FactoryPatern(); // zwracamy konkretną implemtancję, w naszym wypadku to obiekt klasy B task = fp.GetConcreteDoable(); // następnie możemy wywołać w naszej klasie metodę z klasy B task.DoTaskOne(); Console.ReadKey(); } }Takie luźne powiązanie pomiędzy klasami jest również korzystne z punktu widzenia rozwoju aplikacji. Wykorzystując ten wzorzec również klient ma możliwość używania wielu klas zależnych tak długo jak wszystkie klasy implementują przygotowany interfejs.
Przykład wykorzystany nie miał niczego wspólnego z rzeczywistością. Aby lepiej zrozumieć ten wzorzec spróbujmy przygotować
prostą aplikację, która będzie rozwiązwała problem możliwy w rzeczywistości. Załóżmy, że mamy przygotować sklep internetowy,
który pozwala na dwa rodzaje płatności. Pierwsza z metod będzie nazywała się BankOne a druga to
BankTwo. Pierwsza z metod pobiera dodatkowo 2% z karty kredytowej jeżeli zamówienie jest
mniejsze niż 50zł oraz 1% jeżeli zamówienie jest wyższe niż 1%. Z kolei metoda druga pobiera za każdym razem 1.5% prowizji.
Przystąpmy do przygotowania aplikacji. W pierwszej kolejności przygotujemy definicję naszych produktów:
public class Product { public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } }Kolejny krok to przygotowanie interfejsu IPaymentGateway, który będzie w sobie zawierał deklarację metody płatności.
interface IPaymentGateway { void MakePayment(Product product); }W świecie rzeczywistym powinniśmy jeszcze przekazywać informacje o kliencie dokonującym zakupu. Aby uprościć przykład dane takie nie będą przekazywane.
public class BankOne : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą pierwszego sposobu Console.WriteLine("Pierwszy rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } } public class BankTwo : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą drugiego sposobu Console.WriteLine("Drugi rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } }Teraz przyszedł czas na utworzenie klasy fabryki, która będzie zarządzała szczegółowym tworzniem tych obiektów. Aby być w stanie zidentyfikować, który mechanim płatności wybrał użytkownik utworzymy prosty typ wyliczeniowy EPaymentMethod:
enum EPaymentMethod { BANK_ONE, BANK_TWO }Klasa naszej fabryki będzie używała tego typu wyliczeniowego aby zidentyfikować, który obiekt powinien zostać utworzony. Spójrzmy na poniższy przykład:
public class PaymentGatewayFactory { public virtual IPaymentGateway CreatePaymentGateway(EPaymentMethod method, Product prod) { IPaymentGateway gateway = null; switch (method) { case EPaymentMethod.BANK_ONE: gateway = new BankOne(); break; case EPaymentMethod.BANK_TWO: break; gateway = new BankTwo(); default: break; } return gateway; } }Czym zajmuje się powyższa klasa? Powyższa klasa przyjmuje rodzaj płatności dokonany przez użytkownika a następnie na podstawie tego wyboru tworzy konkrenty obiekt.
public class PaymentProcessor { IPaymentGateway gateway = null; // Dokonywanie płatności // Wywołanie metody CreatePaymentGateway(...) zwraca nam obiekt utworzony // w zależności od wyboru rodzaju płatności przez klienta public void MakePayment(EPaymentMethod method, Product product) { PaymentGatewayFactory factory = new PaymentGatewayFactory(); this.gateway = factory.CreatePaymentGateway(method, product); this.gateway.MakePayment(product); } }Możecie zauważyć, że klasa klienta nie zależy od konkretnej implementacji klasy, która jest odpowiedzialna za wykonanie opłaty za zakupiony produkt, tj. BankOne oraz BankTwo. Cała logika jest wydzielona do abstrakcji a schemat taki pokazuje użycie wzorca fabryki.
using System; namespace BankExample { class Program { static void Main(string[] args) { // Tworzenie instancji klasy w której znajduje się metoda do dokonania płatności PaymentProcessor pre = new PaymentProcessor(); // Definiujemy produkt - to jest tylko przykład Product prod = new Product(); prod.Name = "Audi RS6"; prod.Price = 560000; prod.Description = "Bardzo szybkie rodzinne kombi"; // Dokonujemy płatności pierwszym sposobem. // W razie problemów z analizą kodu zachęcam do ponownego zapoznania się z artykułem // oraz dokładnego prześledzenia krok po kroku co dzieje się w kodzie pre.MakePayment(EPaymentMethod.BANK_ONE, prod); Console.ReadKey(); // Wynik działania programu // Pierwszy rodzaj platnosci za Audi RS6, kwota 560000 } } public enum EPaymentMethod { BANK_ONE, BANK_TWO } public class Product { public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } } public interface IPaymentGateway { void MakePayment(Product product); } public class BankOne : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą pierwszego sposobu Console.WriteLine("Pierwszy rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } } public class BankTwo : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą drugiego sposobu Console.WriteLine("Drugi rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } } public class PaymentGatewayFactory { public virtual IPaymentGateway CreatePaymentGateway(EPaymentMethod method, Product prod) { IPaymentGateway gateway = null; switch (method) { case EPaymentMethod.BANK_ONE: gateway = new BankOne(); break; case EPaymentMethod.BANK_TWO: break; gateway = new BankTwo(); default: break; } return gateway; } } public class PaymentProcessor { IPaymentGateway gateway = null; // Dokonywanie płatności // Wywołanie metody CreatePaymentGateway(...) zwraca nam obiekt utworzony // w zależności od wyboru rodzaju płatności przez klienta public void MakePayment(EPaymentMethod method, Product product) { PaymentGatewayFactory factory = new PaymentGatewayFactory(); this.gateway = factory.CreatePaymentGateway(method, product); this.gateway.MakePayment(product); } } }
GoF definiuje metodę fabryki jako: Zdefiniuj interfejs do tworzenia obiektów, jednakże pozwól
aby podklasy decydowały, którą klasę zainicjować. Factory Method (metoda fabryki) pozwala
klasie na odłożenie inicjowania do podklasy.
Spójrzymy na poniższy diagram:
Co reprezentują powyższe klasy?
public enum EPaymentMethod { BANK_ONE, BANK_TWO, PAYPAL, PRZELEWY24 }Teraz możemy posiadać jedną klasę dziedziczącą po PaymentGatewayFactory, która będzie zawierała definicje dla nowych sposobów płatności:
public class PaymentGatewayFactory2 : PaymentGatewayFactory { public virtual IPaymentGateway CreatePaymentGateway(EPaymentMethod method, Product prod) { IPaymentGateway gateway = null; switch (method) { case EPaymentMethod.PAYPAL: // obsługa przelewów przez system Paypal break; case EPaymentMethod.PRZELEWY24: // obsługa przelewów przez system Przelewy24 break; default: // jeżeli nie realizujemy nowego sposobu płatności wywołujemy metodę bazową, // która obsługuje pozostałe rodzaje płatności base.CreatePaymentGateway(method, prod); break; } return gateway; } }Jeżeli teraz chcemy używac nowego sposobu płatności musimy jedynie utworzyć nowy obiekt PaymentGatewayFactory2 zamiast PaymentGatewayFactory. Dzięki temu nasz klient będzie miał dostęp do wszystkich 4 rodzajów płatności:
using System; namespace BankExampleGoF { class Program { static void Main(string[] args) { // Tworzenie instancji klasy w której znajduje się metoda do dokonania płatności PaymentProcessor pre = new PaymentProcessor(); // Definiujemy produkt - to jest tylko przykład Product prod = new Product(); prod.Name = "Audi RS6"; prod.Price = 560000; prod.Description = "Bardzo szybkie rodzinne kombi"; // Dokonujemy płatności pierwszym sposobem. // W razie problemów z analizą kodu zachęcam do ponownego zapoznania się z artykułem // oraz dokładnego prześledzenia krok po kroku co dzieje się w kodzie pre.MakePayment(EPaymentMethod.PAYPAL, prod); Console.ReadKey(); // Wynik działania programu // Pierwszy rodzaj platnosci za Audi RS6, kwota 560000 } } public enum EPaymentMethod { BANK_ONE, BANK_TWO, PAYPAL, PRZELEWY24 } public class Product { public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } } public interface IPaymentGateway { void MakePayment(Product product); } public class BankOne : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą pierwszego sposobu Console.WriteLine("Pierwszy rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } } public class BankTwo : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą drugiego sposobu Console.WriteLine("Drugi rodzaj płatności za {0}, kwota {1}", product.Name, product.Price); } } // W klasie zdefiniowana jest logika obsługi starego rodzaju płatności public class PaymentGatewayFactory { public virtual IPaymentGateway CreatePaymentGateway(EPaymentMethod method, Product prod) { IPaymentGateway gateway = null; switch (method) { case EPaymentMethod.BANK_ONE: gateway = new BankOne(); break; case EPaymentMethod.BANK_TWO: break; gateway = new BankTwo(); default: break; } return gateway; } } public class PaymentGatewayFactory2 : PaymentGatewayFactory { public virtual IPaymentGateway CreatePaymentGateway(EPaymentMethod method, Product prod) { IPaymentGateway gateway = null; switch (method) { case EPaymentMethod.PAYPAL: // obsługa przelewów przez system Paypal break; case EPaymentMethod.PRZELEWY24: // obsługa przelewów przez system Przelewy24 break; default: // jeżeli nie reazlizujemy nowego sposobu płątności wywołujemy metodę bazową, // która obsługuje pozostałe rodzaje płatności base.CreatePaymentGateway(method, prod); break; } return gateway; } } public class PaymentProcessor { IPaymentGateway gateway = null; // Dokonywanie płatności // Wywołanie metody CreatePaymentGateway(...) zwraca nam obiekt utworzony // w zależności od wyboru rodzaju płatności przez klienta public void MakePayment(EPaymentMethod method, Product product) { PaymentGatewayFactory2 factory = new PaymentGatewayFactory2(); this.gateway = factory.CreatePaymentGateway(method, product); // w przkładzie, który został przygotowany nie została przygotowana metoda do "obsługi" // płatności przez PayPal - w tym miejscu wyskoczy nam błąd. Aby tego uniknać należy // przygotować metodę jak poniżej... // oraz w klasie PaymentGatewayFactory2, metodzie: CreatePaymentGateway // dodać kod - > gateway = new Paypal(); this.gateway.MakePayment(product); } } public class Paypal : IPaymentGateway { public void MakePayment(Product product) { // Metoda to pozwala na dokonanie płatności za pomocą trzeciego sposobu Console.WriteLine("Trzeci rodzaj płatności (PayPal) za {0}, kwota {1}", product.Name, product.Price); } } }W powyższym przykładzie nasz Creator, tj. metoda wytwórcza nie jest czystą klasą abstrakcyjną, gdyż dostarcza nam pewną funkcjonalność, która jednak może być przesłonięta przez konkretną fabrykę, która z niej dziedziczy.