Pomysł na ten wpis jest taki, że na początek, napiszę testy jednostkowe, które będą palić się na czerwono, w których zdefiniuje problem. Testy zapalę na zielono poprzez implementację wzorca Adapter.
Adapter przekształca interfejs klas na inny, oczekiwany przez klienta. Adapter umożliwia współdziałanie klasom, które z uwagi na niezgodne interfejsy standardowo nie mogą współdziałać ze sobą.
„Wzorce Projektowe”, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
Adapter dokonuje konwersji danej klasy do postaci innego interfejsu, zgodnie z oczekiwaniami klienta. Adapter pozwala na współpracę klas, które ze względu na niekompatybilne interfejsy wcześniej nie mogły ze sobą współpracować. {…}. Dzięki temu kod klienta nie musi być modyfikowany za każdym razem, kiedy będzie współpracować z innym interfejsem.
„Head First Design Patterns” Erich Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra
Adapter Obiektów
Na początek, napiszę test, w którym zdefiniuję problem. Po czym napiszę kod produkcyjny, który zapali ten test na zielono.
public class AdapterTester { private readonly Client client; public AdapterTester() { client = new Client(); } [Fact] public void call_request_method_in_target() { // arrange ITarget target = A.Fake(); // act client.CallRequest(target); // assert A.CallTo(() => target.Request()).MustHaveHappened(); } }
Clinet
posiada metodę CallRequest
, która w argumencie pobiera obiekt spełniający interfejs ITarget
. W teście sprawdzamy, czy w wyniku wywołania metody CallRequest
została wywołana metoda Request
na obiekcie target
.
Aby ten test zapalił się na zielono potrzebna jest definicja interfejsu ITarget
oraz implementacja klasy Client
.
public interface ITarget { void Request(); } public class Client { public void CallRequest(ITarget target) { target.Request(); } }
Teraz zdefiniuję kolejny test, którego zapalenie na zielono będzie wymagało napisania wzorca Adapter.
public class AdapterTester { // ... pozostały kod jest niezmieniony [Fact] public void call_specific_request_method_in_adaptee() { // arrange IAdaptee adaptee = A.Fake(); ITarget adapter = new Adapter(adaptee); // act client.CallRequest(adapter); // assert A.CallTo(() => adaptee.SpecificRequest()).MustHaveHappened(); } }
W teście tym pojawił się obiekt typu IAdaptee
. IAdaptee
posiada metodę SpecificRequest
, co powoduje, że interfejs IAdaptee
jest inny od ITarget
. Klient nie będzie umieć współpracować z obiektem typu IAdaptee
. IAdaptee
wymaga adaptacji do współdziałania z metodą CallRequest
z klasy Client
.
IAdaptee
przekazany jest do konstruktora nowej klasy Adapter
, która implementuje interfejs ITarget
. Wewnątrz klasy Adapter
nastąpi przetłumaczenie interfejsu IAdaptee
na interfejs ITarget
. W ostatniej linijce testu jest asercja mówiąca o tym, że w wyniku przetłumaczenia IAdaptee
na ITarget
, jeśli klient wywoła metodę Request
na obiekcie typu ITarget
to w rzeczywistości wywoła się metoda SpecificRequest
na obiekcie typu IAdaptee
.
Jak widać w teście, klasa Client
pozostaje bez zmian — to ważne założenie. Adaptacja nowego interfejsu wykonywana jest bez najmniejszej zmiany klasy, do której ten nowy interfejs adaptujemy.
Test pali się teraz na czerwono. Kod się nie kompiluje. Definiuję teraz ten nowy interfejs, który będę adaptować.
public interface IAdaptee { void SpecificRequest(); }
Teraz definiuję klasę Adapter
, która w konstruktorze przyjmuje obiekt typu IAdaptee
. Klasa Adapter
implementuje interfejs ITarget
, czyli ten pierwotny, do którego się adaptujemy.
public class Adapter : ITarget { private readonly IAdaptee adaptee; public Adapter(IAdaptee adaptee) { this.adaptee = adaptee; } public void Request() { adaptee.SpecificRequest(); } }
Fajnie i prosto wytłumaczony wzorzec Adapter.
PolubieniePolubienie
Eleganckie wytłumaczenie 🙂 No i super to wygląda przy wykorzystaniu ‚składni’ frameworków fejkujących (FakeItEasy), prościej zadeklarować co ma się wydarzyć się nie da: „adaptee.SpecificRequest()).MustHaveHappened();”
PolubieniePolubienie