Wzorce projektowe – Adapter
Adapter jest wzorcem strukturalnym szczególnie przydatnym w sytuacji, gdy pracujemy z zewnętrznymi bibliotekami, systemami API, klasami, itp. Pozwala w łatwy sposób połączyć dwa systemy o niekompatybilnych ze sobą interfejsach. Artykuł ten przedstawia idee działania tego wzorca projektowego i sposób jego użycia.
Charakterystyka wzorca adapter
Adapter, czyli tłumacząc na język polski „przejściówka”, spełnia rolę łącznika pomiędzy niedostosowanymi do siebie systemami tak, aby możliwa była współpraca między nimi. Wzorzec ten szczególnie wykorzystywany jest, gdy chcemy korzystać z zewnętrznych bibliotek i systemów, których interfejsy nie są dostosowane do naszej aplikacji. Przy pomocy adaptera opakowujemy niekompatybilny interfejs takiej biblioteki w nowy i dzięki temu nie musimy modyfikować naszego kodu.
Adapter w swojej istocie jest bardzo prostym wzorcem projektowym, co pokazuje poniższy diagram klas UML.
Jak widzimy na diagramie, interfejs naszego systemu jest implementowany przez konkretne klasy. Chcąc wykorzystać w aplikacji zewnętrzny element, np. bibliotekę lub klasa napisana przez kogoś innego, musimy dostosować ten element do naszego systemu. I tu z pomocą przychodzi nam adapter, który opakowuje poszczególne operacje klasy zewnętrznej, implementując znany nam interfejs.
Przykład zastosowania wzorca adapter – wojna robotów
Poniższy przykład w bardzo prosty sposób pokaże ideę wykorzystania omawianego wzorca projektowego. Załóżmy, że piszemy fragment gry komputerowej, w której występują różne wrogie jednostki, tj. czołg lub robot. Każda z tych jednostek ma możliwość użycia swojej broni, jazdy do przodu oraz przypisania operatora, sterującego tą jednostką.
Zacznijmy od napisania prostego interfejsu realizującego te zadania.
1 2 3 4 5 6 7 8 |
namespace DesignPatterns\Adapter\Robot; interface EnemyAttacker { public function fireWeapon(); public function driveForward(); public function assignDriver($driverName); } |
Następnie stwórzmy pierwszy typ jednostki. Będzie to Czołg, czyli Tank.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Adapter\Robot; use DesignPatterns\Adapter\Robot\EnemyAttacker; class EnemyTank implements EnemyAttacker { public function fireWeapon() { $attackDamage = rand(1,10); echo 'Enemy Tank does '.$attackDamage.' damage'; } public function driveForward() { $movement = rand(1,5); echo 'Enemy Tank moves '.$movement.' spaces'; } public function assignDriver($driverName) { echo $driverName.' is driving the Enemy Tank'; } } |
Na końcu stwórzmy klienta, który przetestuje działanie wrogiego czołgu.
1 2 3 4 5 6 7 8 9 10 |
require_once '../../../vendor/autoload.php'; use DesignPatterns\Adapter\Robot\EnemyTank; $rx7Tank = new EnemyTank(); echo 'The Enemy Tank:'; $rx7Tank->assignDriver('Frank'); $rx7Tank->driveForward(); $rx7Tank->fireWeapon(); |
Wynik działania poszczególnych operacji jest następujący:
1 2 3 4 |
The Enemy Tank: Frank is driving the Enemy Tank Enemy Tank moves 1 spaces Enemy Tank does 3 damage |
Jak widać system jest prosty i przejrzysty. W dalszej kolejności chcemy napisać kolejną jednostkę o nazwie Robot. Okazało się, że istnieje już klasa, która jest dostępna i może być wykorzystana w naszym projekcie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Adapter\Robot; class EnemyRobot { public function smashWithHands() { $attackDamage = rand(1,10); echo 'Enemy Robot causes '.$attackDamage.' damage with its hands'; } public function walkForward() { $movement = rand(1,5); echo 'Enemy Robot walks forward '.$movement.' spaces'; } public function reactToHuman($driverName) { echo 'Enemy Robot tramps on '.$driverName; } } |
Jak widzimy, powyższa klasa implementuje całkowicie odmienny interfejs. Oczywiście moglibyśmy próbować poprawiać zewnętrzne klasy tak, aby dopasować je do naszego systemu, jednak jest to bardzo zły pomysł. Takie rozwiązanie jest czasochłonne, stwarza wiele problemów i chaosu w kodzie. Ponadto może generować wiele błędów. O wiele lepszym rozwiązaniem jest napisanie Adaptera, który dostosuje zewnętrzną klasę do naszej aplikacji.
Poniżej zaprezentowana jest klasa adaptera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
namespace DesignPatterns\Adapter\Robot; use DesignPatterns\Adapter\Robot\EnemyAttacker; class EnemyRobotAdapter implements EnemyAttacker { private $theRobot; public function __construct(EnemyRobot $newRobot) { $this->theRobot = $newRobot; } public function fireWeapon() { $this->theRobot->smashWithHands(); } public function driveForward() { $this->theRobot->walkForward(); } public function assignDriver($driverName) { $this->theRobot->reactToHuman($driverName); } } |
Oczywiście powyższy przykład jest bardzo prosty, ponieważ chciałem pokazać samą istotę działania wzorca adaptera. W realnych projektach, adapter może być bardzo skomplikowaną klasą lub nawet szeregiem klas.
Rozszerzmy teraz klienta, tak aby sprawdzić działania Robota. Widzimy, że przy pomocy adaptera, możemy wykorzystać nową jednostkę atakującą w taki sam sposób jak pozostałe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use DesignPatterns\Adapter\Robot\EnemyTank; use DesignPatterns\Adapter\Robot\EnemyRobot; use DesignPatterns\Adapter\Robot\EnemyRobotAdapter; $rx7Tank = new EnemyTank(); echo 'The Enemy Tank:'; $rx7Tank->assignDriver('Frank'); $rx7Tank->driveForward(); $rx7Tank->fireWeapon(); $fredTheRobot = new EnemyRobot(); $robotAdapter = new EnemyRobotAdapter($fredTheRobot); echo 'The Robot:'; $robotAdapter->assignDriver('Mark'); $robotAdapter->driveForward(); $robotAdapter->fireWeapon(); |
Wynik działania aplikacji jest następujący:
1 2 3 4 5 6 7 8 9 |
The Enemy Tank: Frank is driving the Enemy Tank Enemy Tank moves 1 spaces Enemy Tank does 3 damage The Robot with Adapter: Enemy Robot tramps on Mark Enemy Robot walks forward 2 spaces Enemy Robot causes 10 damage with its hands |
Powyższy przykład można pobrać tutaj: adapter-robot
Przykład na GitHub: https://github.com/molitorys/design-patterns/tree/master/src/Adapter/Robot
Podsumowanie
Wzorzec projektowy adapter jest według mnie bardzo ważnym i przydatnym wzorce. Jego znajomość bardzo ułatwia pracę z zewnętrznymi bibliotekami i systemami oraz pomaga uniknąć wiele niepotrzebnych kombinacji. Adapter doskonale oddziela naszą aplikację od obcych elementów i pozwala na łatwe i przejrzyste dopasowanie różnych części programu.