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.