Nadszedł czas na kolejny prosty wzorzec projektowy: strategia (ang. strategy). W jakiej sytuacji może nam pomóc? Wyobraź sobie sklep internetowy i jego system liczenia marży. Warto dać klientowi możliwość wyboru sposobu naliczenia marży w zależności od różnych parametrów, a sobie zapewnić opcję łatwego dodania kolejnego algorytmu wyliczania cen.
Marże mogą być liczone w różny sposób
– w zależności od tego, kto jest
producentem bądź dostawcą produktu,
w zależności od ceny produktu itd. Jednym
z rozwiązań, które przychodzą do głowy zaraz po
przeczytaniu powyższych warunków, jest drabinka
instrukcji warunkowych. Wewnątrz nich liczona
będzie marża wedle algorytmu wybranego przez
administratora sklepu:
$fltCena = pobierzCene();
if ($intAlgorytm == PRODUCENT) {
// liczenie marży odpowiednim algorytmem
}
elseif ($intAlgorytm == CENA) {
// liczenie marży odpowiednim algorytmem
}
elseif ($intAlgorytm == KLIENT) {
// liczenie marży odpowiednim algorytmem
}
$fltCena = pobierzCene();
if ($intAlgorytm == PRODUCENT) {
// liczenie marży odpowiednim algorytmem
}
elseif ($intAlgorytm == CENA) {
// liczenie marży odpowiednim algorytmem
}
elseif ($intAlgorytm == KLIENT) {
// liczenie marży odpowiednim algorytmem
}
Wady powyższego rozwiązania widać już na
pierwszy rzut oka. W razie konieczności dodania
nowego algorytmu obliczania marży, drzewko
warunków wzbogaci się o kolejną instrukcję
warunkową. Już przy kliku algorytmach taki zapis
staje się nieczytelny i trudny w pielęgnacji. Można
co prawda same algorytmy zapisać w osobnych
metodach:
$fltCena = pobierzCene();
if ($intAlgorytm == PRODUCENT) {
$fltCena = liczMarzePoProducencie($fltCena, $intProducent);
}
elseif ($intAlgorytm == CENA) {
$fltCena = liczMarzePoCenie($fltCena);
}
elseif ($intAlgorytm == KLIENT) {
$fltCena = liczMarzePoKliencie($fltCena,$intKlient);
}
float cena = produkt.pobierzCene();
if (algorytm == PRODUCENT) {
cena = liczMarzePoProducencie(cena,producent);
}
else if (algorytm == CENA) {
cena = liczMarzePoCenie(cena);
}
else if (algorytm == KLIENT) {
cena = liczMarzePoKliencie(cena,klient);
}
Jednak i to rozwiązanie nie jest szczególnie
eleganckie, gdy mamy do czynienia z programowaniem
obiektowym. Zastosujmy więc wzorzec
projektowy Strategia.
Wzorzec Strategia
Strategia jest swoistym przepisem na szybkie
wymienianie algorytmów robiących to samo
w różny sposób. Nie musi to być wyłącznie liczenie
marż. Może to być sortowanie (np. w zależności
od wymagań i dostępnych zasobów można wybrać
sortowanie szybsze, powodujące jednak większe
zużycie pamięci operacyjnej, bądź wolniejsze lecz
oszczędzające pamięć), wyszukiwanie czy inne bardziej
zaawansowane algorytmy, jak np. filtrowanie
danych.
Tradycyjnie już zacznijmy od przykładu.
interface Marza {
public function liczCeneZMarza($objProduktu, $objKlienta);
}
class MarzaPorducencenta implements Marza
{
public function liczCeneZMarza($objProduktu, $objKlienta) {
//liczenie marży na podstawie danych z obiektu $objProduktu opisującego produkt
return $fltCenaZMarza;
}
}
class MarzaZaleznaOdCeny implements Marza
{
public function liczCeneZMarza($objProduktu, $objKlienta) {
//liczenie marży na podstawie danych z obiektu $objProduktu opisującego produkt
return $fltCenaZMarza;
}
}
class MarzaZaleznaOdKlienta implements
Marza {
public function liczCeneZMarza($objProduktu, $objKlienta) {
//liczenie marży na podstawie danych z obiektu $objProduktu opisującego produkt
//oraz $objKlienta opisującego klienta
return $fltCenaZMarza;
}
}
class Produkt {
private $objMarza;
private $objKlient;
public function __construct(Marza $objMarza, Klient $objKlient) {
$this->objMarza = $objMarza;
$this->objKlient = $objKlient;
}
public function cenaBrutto() {
return $this->objMarza->liczCeneZMarza($this, $this->objKlient);
}
public function cenaZakupuBrutto() {
//wydobycie ceny brutto do której musi zostać dodana marża
return $fltCena;
}
}
interface Marza {
void Float liczCeneZMarza(Produkt produkt, Klient klient);
}
public class MarzaPorducencenta implements
Marza {
public Float liczCeneZMarza(Produkt
produkt, Klient klient) {
Float cenaZMarza;
//liczenie marży na podstawie danych z obiektu produkt opisującego produkt return cenaZMarza;
}
}
public class MarzaZaleznaOdCeny implements
Marza {
public Float liczCeneZMarza(Produkt produkt, Klient klient) {
Float cenaZMarza;
//liczenie marży na podstawie danych z obiektu produkt opisującego produkt
return cenaZMarza;
}
}
public class MarzaZaleznaOdKlienta
implements Marza {
public Float liczCeneZMarza(Produkt produkt, Klient klient) {
Float cenaZMarza;
//liczenie marży na podstawie danych z obiektu produkt opisującego produkt
//oraz klient opisującego klienta
return cenaZMarza;
}
}
public class Produkt {
private Marza marza;
private Klient klient;
public Produkt(Marza marza, Klient klient) {
this.marza = marza;
this.klient = klient;
}
public Float cenaBrutto() {
return marza.liczCeneZMarza(this, klient);
}
public Float cenaZakupuBrutto() {
//wydobycie ceny brutto do której musi zostać dodana marża
return cena;
}
}
Krótka analiza
Algorytmy zostały zaprogramowane w osobnych
klasach ze wspólnym interfejsem. Dzięki
wspólnemu interfejsowi kod używający algorytmu
nie musi wiedzieć, z jakiego algorytmu korzysta.
Co więcej, algorytmy te można podmieniać w czasie
działania programu.
W naszym prostym przykładzie obiekt
algorytmu jest przekazywany do klasy Produkt
w konstruktorze. Można jednak dodać tu metodę
pozwalającą na jego wymianę podczas działania
programu.
Cały kod jest bardzo prosty. W analogiczny
sposób, jak ma to miejsce w zaprezentowanych
wyżej trzech sposobach liczenia marż, można dodać kolejne algorytmy wyliczania marży
i ceny.
Podsumowanie
Jak już zapewne nieraz się przekonałeś, wzorce
projektowe w dużej części są bardzo proste. Mimo
iż czasami wydaje się, że utrudniają one programowanie
i niepotrzebnie każą wprowadzać szereg
nowych klas, po ich zastosowaniu okazuje się, że
dzięki nim projekt aplikacji jest bardziej elastyczny,
podatniejszy na rozwój i łatwiejszy w pielęgnacji.
Pamiętaj też, że wzorce projektowe można, a nawet
trzeba łączyć.