W wielkim skrócie, wzorzec projektowy adapter, daje nam możliwość na współpracę klas lub interfejsów niezgodnych z naszą aplikacją.
Wytłumaczę Ci zasadę działania adaptera na bardzo prostym przykładzie. Między wtyczkami w USA a Europie istnieje bardzo duża różnica. Przyjeżdżając do USA musimy być świadomi że nie naładujemy telefonu identyczną ładowarką co w Europie. W takich sytuacjach przychodzi nam z pomocą adapter, który zmieni naszą wtyczkę europejską na amerykańską. Proste, prawda?
Ale jak adapter może nam pomóc?
Wykorzystanie adaptera pokażę Ci na prostym przykładzie napisanym w PHP. Załóżmy że chcemy na okres pewnego eventu zmienić system naliczania rabatu dla użytkownika.
Obecnie dla każdej osoby która wydała więcej niż 1tyś zł dajemy 3% rabatu, ale na okres eventu firma zdecydowała że za każde 500zł dajemy 1.5%, rabatu z czego maksymalny rabat może wynosić 10%. W tym momencie wkracza na pomoc wzorzec projektowy adapter, ale zacznijmy od przykładu w kodzie. Następnie przejdziemy do teorii w czym on nam pomógł.
interface RebatePolicyInterface
{
public function getRebatePrice(float $price, float $sumMoneySpent): float;
}
class RebatePolicy implements RebatePolicyInterface
{
public function getRebatePrice(float $price, float $sumMoneySpent): float
{
if ($sumMoneySpent <= 1000) {
return $price;
}
return $sumMoneySpent * 0.97;
}
}
class SpecialRebatePolicyAdapter implements RebatePolicyInterface
{
/**
* @var RebatePolicyInterface
*/
private $rebatePolicy;
public function __construct(RebatePolicyInterface $rebatePolicy)
{
$this->rebatePolicy = $rebatePolicy;
}
public function getRebatePrice(float $price, float $sumMoneySpent): float
{
return $this->rebatePolicy->getRebatePrice($price, $sumMoneySpent);
}
}
interface SpecialRebatePolicyInterface
{
public function getSpecialRebatePrice(float $price, float $sumMoneySpent): float;
}
class EventRebatePolicy implements SpecialRebatePolicyInterface
{
public function getSpecialRebatePrice(float $price, float $sumMoneySpent): float
{
if ($sumMoneySpent > 5000) {
return $price * 0.90;
}
$rebate = (1.5 * ((int) ($sumMoneySpent / 500))) / 100;
return $price * (1 - $rebate);
}
}
W tym momencie nasza klasa jest uzależniona tylko od interfejsu RebatePolicyInterface. Daje nam to teraz możliwość szybkiego przełączenia logiki biznesowej przydzielania rabatów na nową. Np. w symfony zrobimy to w bardzo prosty sposób tj.
# Before
services:
App\RebatePolicyInterface:
class: 'App\RebatePolicy'
# After use Adapter
services:
App\RebatePolicyInterface:
class: 'App\SpecialRebatePolicyAdapter'
arguments:
- '@App\EventRebatePolicy'
Dodatkowo możemy dodawać kolejne implementacje dla naszego adaptera (spełniamy dzięki temu zasadę SOLID, a dokładnie Open/Closed), np. ChristmasRebatePolicy. Jest to banalnie proste a zarazem daje nam duże pole manewru do szybkich zmian.
class ChristmasRebatePolicy implements SpecialRebatePolicyInterface
{
public function getSpecialRebatePrice(float $price, float $sumMoneySpent): float
{
//Some business logic
}
}
services:
App\RebatePolicyInterface:
class: 'App\SpecialRebatePolicyAdapter'
arguments:
- '@App\ChristmasRebatePolicy'
Co nam dało zastosowanie adaptera
Patrząc na kod wyżej, widzimy że dodajemy interfejsowi kolejną implementacje, co daję nam dostosowanie starego kodu naliczania rabatu do nowych wymogów biznesowych.
Ale przecież możemy dopisać logikę wraz z metodą np. isEvent i po problemie, otóż nie. Robiąc w ten sposób nie spełniamy zasad SOLID. Dokładniej mówiąc, chodzi o Open/Closed oraz według mnie SRP, ponieważ wtedy klasa ma dwa powody do zmiany, tj. dobieranie polityki rabatów oraz sama polityka rabatowa.
Nasz adapter usunął w tym momencie zbędna logikę biznesową z naszego systemu oraz spełnił dzięki temu dwie zasady SOLID. Dodatkowo nasz kod stał się dużo czystszy, ponieważ mamy dwie różne klasy i każda ma swój pojedynczy obowiązek, zaś dobieraniem polityki zajmuję się kontener DI.
Nie tylko same plusy
Im więcej klas tym coraz większa złożoność naszego kodu. Dodatkowo nie raz będziemy musieli dodać specjalny mały interfejs (co nie jest złą praktyką, ponieważ lepiej posiadać 5 małych interfejsów niż 1 ogromny) / klasę abstrakcyjną / klasę który/-a będzie obsługiwał/-a możliwość dodania adaptera.
Drugim minusem jest możliwość przesadzania w drugą stronę i dodawania do wszystkiego adapterów. Trzeba we wszystkim zachować zdrowy rozsądek. Są sytuacje w których adapter lepiej się sprawuję od dodania dodatkowego kodu, ale są również sytuacje gdy dopisanie kodu jest lepsze, optymalniejsze itp.
Podsumowanie
Dzisiejszy wpis, dał Ci podstawową wiedzę na temat wzorca projektowego adapter. Oczywiście przypadek wymieniony w tym wpisie, nie jest wszystkimi możliwymi sytuacjami użycia adaptera. Nie wymieniłem tutaj m.in. sytuacji dopisania specjalnej metody do klasy bazowej np. dodania do bazowej klasy repozytoriów metody getOneBy (co nie jest do końca dobrą praktyką) lub tłumaczenia naszego kodu między kodem zewnętrznej firmy itd.
Wszystko tak naprawdę zależy od sytuacji, więc to my programiści/programistki, musimy decydować czy w tym danym przypadku posiadamy argument za użyciem adaptera lub go nie posiadamy i musimy daną sytuację obejść w inny sposób np. dopisaniem logiki z prostym if’em etc.
Pokazane wyżej wpisy są bardzo proste i mają tylko przedstawić sposób implementacji, podobnie jak wiele dokumentacji. 🙂
Wiedzę czerpałem z:
https://sourcemaking.com/design_patterns/adapter
Zachęcam również do przeczytania mojego ostatniego wpisu na temat RabbitMQ.
Pozdrawiam, Łukasz 🙂