Nazwa naszego dzisiejszego gościa może z biegu sugerować do czego jest on przeznaczony. Wzorzec adapter, bo o nim mowa, przeznaczony jest do konwersji interfejsu danej klasy do postaci interfejsu oczekiwanego przez klienta. Zabieg ten umożliwia współpracę klas, które wcześniej nie miały możliwości razem współdziałać.
Proszę wyobrazić sobie następującą sytuację. Marek, posiadacz nie aż tak popularnego w Polsce telefonu marki Apple, wybiera się na domówkę. Na miejscu okazuje się, że jest niesamowicie, więc postanawia wszystko uwiecznić na zdjęciach. Niestety przypomina sobie, że przed wyjściem zapomniał naładować swój telefon, co krzyżuje mu plany. Bohater historii rozpaczliwie zaczyna pytać innych uczestników imprezy o ładowarkę. Na jego nieszczęście jedyna jaką udaje mu się doszukać posiada niekompatybilne z jego telefonem złącze typu micro USB. Sytuacja wydaje się beznadziejna, jednakże Marek nauczony błędami przeszłości, jest przygotowany na taką ewentualność. Posiada odpowiedni adapter (rys. 1). Na podobnej zasadzie działa omawiany we wpisie wzorzec.
Diagram UML wzorca adapter wygląda następująco:
Jeśli chcielibyśmy przypisać role z diagramu obiektom z powyższego przykładu, to obiektem docelowym byłby kabel zasilający ze złączem typu Lightning, obiektem adaptowanym kabel zasilający ze złączem typu micro USB, a adapterem – adapter przedstawiony na rys. 1.
Oto jak prezentuje się kod takiego rozwiązania. Zacznijmy od utworzenia interfejsu klasy AppleCharger oraz jej klasy podrzędnej LightningAdapter:
public interface AppleCharger { public void chargeApplePhone(); }
public class LightningCharger implements AppleCharger { @Override public void chargeApplePhone() { System.out.println("I'm a charging iPhone."); } }
Kolejne będą interfejs klasy adaptowanej AndroidCharger wraz z klasą podrzędną MicroUsbCharger:
public interface AndroidCharger { public void chargeAndroidPhone(); }
public class MicroUsbCharger implements AndroidCharger{ @Override public void chargeAndroidPhone() { System.out.println("I'm a charging Android phone."); } }
W celu naładowania telefonu konieczne jest skorzystanie z adaptera, który spełni rolę pośrednika dokonującego konwersji żądań Marka. Posiada on zaimplementowany interfejs takiego typu, do jakiego się adaptuje i przechowuje instancję obiektu adaptowanego:
public class MicroUsbChargerAdapter implements AppleCharger { MicroUsbCharger microUsbCharger; public MicroUsbChargerAdapter(MicroUsbCharger microUsbCharger) { this.microUsbCharger = microUsbCharger; } @Override public void chargeApplePhone() { microUsbCharger.chargeAndroidPhone(); System.out.println("Ykhm.. I mean, a charging iPhone."); } }
Na koniec pozostaje utworzyć krótkie środowisko testowe dla naszego adaptera. Zawiera ono metodę testującą ładowarki do telefonów marki Applem która pobiera obiekt typu AppleCharger i wywołuje jego metodę chargeApplePhone(). Jak widać wykorzystanie adaptera umożliwiło „wciśnięcie” ładowarki ze złączem typu Micro USB, zamiast ładowarki ze złączem Lightning. Metoda ta nigdy nie zorientuje się o tym „przekręcie”.
public class TestingPhoneCharging { public static void main(String[] args) { MicroUsbCharger microUsbCharger = new MicroUsbCharger(); LightningCharger lightningCharger = new LightningCharger(); AppleCharger microUsbChargerAdapter = new MicroUsbChargerAdapter(microUsbCharger); System.out.println("The Android charger says:"); microUsbCharger.chargeAndroidPhone(); System.out.println("\nThe iPhone charger says:"); testIphoneCharing(lightningCharger); System.out.println("\nThe microUsbChargerAdapter says:"); testIphoneCharing(microUsbChargerAdapter); } static void testIphoneCharing(AppleCharger appleCharger) { appleCharger.chargeApplePhone(); } }
Wynik wykonania kodu jest następujący:
The Android charger says: I'm a Micro USB Charger. The iPhone charger says: I'm a Lightning charger. The microUsbChargerAdapter says: I'm a Micro USB Charger. Ykhm.. I mean, a Lightning charger.
W powyższym przykładzie zauważyć można, metodę testującą ładowarki do telefonów marki Apple. Pobiera ona obiekt typu AppleCharger i wywołuje jego metodę chargeApplePhone(). Jak widać wykorzystanie adaptera umożliwiło „wciśnięcie” ładowarki ze złączem typu Micro USB, zamiast ładowarki ze złączem Lightning. Metoda ta nigdy nie zorientuje się o tym „przekręcie”.
Podsumowując, wzorzec adapter można stosować wszędzie tam, gdzie wymagane jest skorzystanie z klasy o niewłaściwym interfejsie. Dokonuje konwersji interfejsu do postaci zgodnej z wymaganiami klienta. Należy jednak pamiętać, że przykład z wpisu był prosty. Niekiedy interfejs docelowy potrafi być bardzo złożony, a wtedy implementacja adaptera może wymagać dużych nakładów pracy.
Cały kod z wpisu znajduje się na moim Githubie.
Źródła literaturowe:
[1] Eric Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates: Wzorce projektowe. Rusz głową!