IT eSky.pl

RSS

Mały… a potrafi! czyli Adapter

W ostatnim czasie przez bloga przewinęły się wpisy, w których został wspomniany wzorzec Adaptera. Jako, że jego tematyka nie została rozwinięta, postanowiłem zebrać garść informacji o tym wzorcu.

Adapter. Bardzo prosty wzorzec projektowy, “przeplotka”. Z pozoru nic szczególnego i nie ma się czym zachwycać, ale jak wiadomo.. pozory mylą.  Gdyby przyrównać wszystkie wzorce do grupy uczestników australijskiego ultra maratonu w 1983 Sydney – Melbourne, Adapter byłby niewątpliwie samym Cliffem Youngiem! Łączy ich to, że obaj są prości w swojej istocie, a może i czasem niedoceniani. Niesłusznie, bo mogą wiele.

OK, przejdźmy do gęstego.

Adapter jest to strukturalny wzorzec projektowy, użyteczny gdy dwa niekompatybilne interfejsy muszą współpracować razem. Tworzy dodatkową warstwę pośrednią, definiuje nowy interfejs do istniejącego obiektu, aby dopasować go do tego, czego wymaga klient.

William Sanders w swojej książce PHP. Wzorce projektowe adapter opisał tak:

Możesz myśleć o wzorcu Adapter jak o poradni dla małżeństw — niweluje różnice, tworząc wspólny interfejs. Dzięki niemu odrębne części nie muszą być przebudowywane, a mimo to mogą współpracować.

Ponadto wzorzec adapter jest jednym ze sposobów zapewniania Interface Segregation Principle. Jest też bardzo ważnym elementem w clean/hexagonal/onion architecture, której stosowanie może znacznie uprościć pisanie odizolowanych testów.

Implementacja:

Istnieją 2 formy wzorca Adaptera

Forma obiektowa

Stosowanie takiej implementacji posiada następujące plusy:

  • łatwa w testowaniu
  • klienta można uzależnić od interfejsu a nie od konkretnej klasy
  • brak silnej zależności po między adapterem i klasą adoptowaną zarówno w kodzie aplikacji jak i w testach

Jak powszechnie wiadomo, nie licząc kiełbaski z grilla z piwem, nic nie jest idealne. Tak jest i w przypadku tej implementacji. Posiada ona następujące minusy:

  • trzeba stworzyc nową klasę, tworzyć jej instancje i zwracac z nią obiekt bedacy przedmiotem dostosowania
  • większe zużycie zasobow

Forma klasowa

Taka implementacja moim zdaniem ma tylko jedną zaletę. Jest łatwiejsza w użyciu niż forma obiektowa.

Natomiast posiada kilka wad:

  • wprowadza silną zależność pomiędzy adapterem i klasą adaptowaną (zarówno w kodzie aplikacji jak i w testach) co może prowadzić do tego, że
  • zmiany w metodach klasy adaptowanej, które nie są wykorzystywane w adapterze, mogą mieć wpływ na działanie adaptera
  • mniej wygodna w testowaniu

Aby przekonać się jak każda z implementacji może wpłynąć na kształt naszych testów, przyjrzyjmy się poniższemu przykładowi.

Testy formy obiektowej

Testy adaptera formy obiektowej możemy wykonać w zupełnej izolacji od obiektu adaptowanego używając mockowania. Zakładam, że klasa Invoice istniała już w systemie, więc testy dla niej już istnieją.

Poniższy przykład przedstawia próbę testowania

Formy klasowej

Testując adapter formy klasowej musimy zadbać o to, aby zależności klasy adaptowanej zostały spełnione. Jest to tym bardziej problematyczne, im więcej ta klasa posiada zależności. W powyższym przykładzie musieliśmy stworzyć mocka obiektu klasy Items, który nie ma znaczenia dla działania testowanej metody. Ponadto nie jesteśmy w stanie sprawdzić wywołań metod getPayerStreet i getPayerHauseNo, z których korzysta testowana metoda.

Wzorzec Adapter a Liskov Substitution Principle

Podczas używania adapterów do tłumaczenia interfejsów trzeba zwrócić uwagę na to, czy nasze rozwiązanie nie łamie zasady Liskov (LSP). Czasem łatwo można przeoczyć jej złamanie.

Przykład: Załóżmy że w naszym systemie potrzebujemy użyć listy. Nasze wymaganie jest takie, że czasem obiekty do niej przekazywane powinny byc przechowywane w pamięci, a czasem w danych sesji w zależności od potrzeby. W naszym systemie istnieje już klasa realizująca operująca na pamięci – GenericList,

do przechowywania danych w sesji decydujemy się na użycie zewnętrznej klasy ExternalListInSession jej interfejs wygląda następująco:

Podczas implementacji rozwiązania naszego problemu chcemy mieć możliwość wymiany sposobu przechowywania danych w liście więc tworzymy interfejs List, którym będziemy się posługiwać.

Aby dopasować do niego wcześniej wspomniane klasy musimy stworzyc dla nich adaptery implementujące interfejs. Czyli przykładowo:

Jak narazie wszystko wygląda OK. Załóżmy że pojawia się potrzeba dodania kolejnej listy. Tym razem zależy nam aby dane były przechowywane w bazie danych. Tutaj także decydujemy się na korzystanie z gotowego rozwiązania DatabaseList… i tutaj pojawia się problem. W przeciwieństwie do poprzednich klas DatabaseList w metodzie insert wymaga aby przekazany parametr był konkretnego typu: DatabaseListItem.

Stworzenie adaptera analogicznego do porzednich:

Zaowocuje to złamaniem zasady LSP. Klient korzystający z metody List::add() nie wie, czy dany obiekt jest obiektem implementującym DatabaseListItem. Natomiast implementacja sprawdzenia typu obiektu w kliencie, walidacji w adapterze czy dodanie typowania w metodzie ListInDatabsae::add

jest naruszeniem zasady LSP.

Co z tym zrobić?

Rozwiązaniem tego problemu może być użycie dodatkowej klasy mapera, która mogłaby być zależnością naszego adaptera. Przykładowo:

Jak widać wzorzec Adaptera pomimo bardzo prostej formy jest niebywale użyteczny i ma realny wpływ na przejrzystość naszego systemu oraz formę testów. Podsumowując, Adapter jest jak nieskończone życie w Mario, jest jak kopnięcie z półobrotu Chucka Norrisa, jest jak epic split Jean Claude Van Damme’a, jest jak gol z przewrotki Ibrahimovicza, jest jak oscypek z grilla, z kiełbaską, z grilla z piwem…

po prostu wymiata!

Źródła:

  • Agile programowanie zwinne’ – Robert C. Martin, Micah Martin
  • PHP. Wzorce projektowe’ – William Sanders
  • Professional PHP Design Patterns’ – Aaron Saray
  • https://github.com/domnikl/DesignPatternsPHP
  • życie ;)