Wzorzec projektowy Adapter jest bardzo prostym w implementacji i użyciu wzorcem projektowym, a jednocześnie powszechnie stosowanym. W tym wpisie pokażę Ci, do czego można wykorzystać adapter oraz przedstawię przykładową implementację Adaptera w TypeScript. Dołożyłem wszelkich starań, by przykłady były zrozumiałe i łatwe do przełożenia na inne powszechnie wykorzystywane języki obiektowe.
Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Zastosowanie
Mówiąc o Adapterze, można skojarzyć go z adapterami do gniazdek elektrycznych czy popularnych portów w urządzeniach elektronicznych. Jest to skojarzenie jak najbardziej słuszne. Adapter w inżynierii oprogramowania, tak jak w codziennym życiu, służy do połączenia ze sobą dwóch niekompatybilnych interfejsów. Najprostsza implementacja wzorca Adapter przedstawiona została na poniższym diagramie UML.
Pierwsza klasa z powyższego diagramu to klient, czyli użytkownik adaptera. Jego rolą jest jedynie wywołanie metody doSomething()
. Znacznie ciekawsze klasy to Adapter
ora LegacyClass
.
Załóżmy, że LegacyClass
jest klasą używaną w ogromnym systemie i liczba jego wystąpień jest liczona w setkach. Ponadto klasa LegacyClass
pochodzi spoza organizacji (na przykład z zewnętrznej zależności), a jej struktura nie może zostać zmieniona. Parametry, jakie przyjmuje metoda doSomething
to trzy wartości string oraz opcjonalny parametr force
z domyślną wartością false
.
Po jakimś czasie uznano, że parametr force
musi zostać ustawiony na wartość true w całym systemie oraz że wygodniej będzie przekazywać wartości w postaci obiektu, zamiast listy parametrów. W przypadku zmiany parametru force
rozwiązaniem byłaby ręczna podmiana tego parametru w każdym miejscu, gdzie wywołano metodę doSomething()
. Jednak znacznie lepszym rozwiązaniem jest zastąpienie LegacyClass
adapterem. Jest to zabezpieczenie na przyszłość w przypadku kolejnych zmian. W takim wypadku wprowadzenie zmiany będzie wymagało zmiany w jednym miejscu.
Jeśli zaś chodzi o listę parametrów w postaci obiektu, to zmiana w adapterze została podyktowana komfortem pracy programisty. Zakładając, że kolejność podanych parametrów ma znaczenie, to przekazanie listy parametrów zdejmuje ciężar z programisty o pamiętaniu kolejności. Oczywiście sprawdzenie kolejności parametrów to nie jest problem. Natomiast warto pamiętać, że takiego błędu kompilator nie wyłapie, a ludzki umysł bywa zawodny.
Przykładowa implementacja
W celu praktycznego pokazania wykorzystania wzorca Adapter pokażę przykładową implementację adaptera do klienta HTTP w TypeScript. Przykład implementuje klasy z poniższego diagramu UML.
Jest to przykład ekstremalnie uproszczony, pozbawiony nadmiarowej logiki. Sam przykład zaś wygląda następująco.
interface IHttpClient {
get( url: string ): Promise<HttpResponse>;
post( url: string ): Promise<HttpResponse>;
put( url: string ): Promise<HttpResponse>;
delete( url: string ): Promise<HttpResponse>;
}
class Client {
public constructor(
private readonly _httpClient: IHtrpClient
) {}
public async call(): Promise<void> {
const { statusCode } = await this._httpClient.get('localhost:808/status');
if ( statusCode !== 200 ) {
throw new Error( 'Service unavailable!' );
}
}
}
class HttpClient implements IHttpClient {
public constructor(
private readonly _legacyHttpClient: LegacyHttpClient = new LegacyHttpClient()
) {}
public get( url: string ): Promise<HttpResponse> {
return this._legacyHttpClient.request( url, 'GET', { headers: this._getHttpHeaders() } )
}
public post( url: string ): Promise<HttpResponse>{
return this._legacyHttpClient.request( url, 'POST', { headers: this._getHttpHeaders() } )
}
public put( url: string ): Promise<HttpResponse> {
return this._legacyHttpClient.request( url, 'PUT', { headers: this._getHttpHeaders() } )
}
public delete( url: string ): Promise<HttpResponse> {
return this._legacyHttpClient.request( url, 'DELETE', { headers: this._getHttpHeaders() } )
}
private _getHttpHeaders(): Record<string, unknown> {
return { /* HTTP Headers*/ }
}
}
class LegacyHttpClient {
public request( url: string, method: string, data: Record<string, unknown> ): HttpResponse {
// magic things...
}
}
Tak zaprojektowane rozwiązanie sprawia, że LegacyHttpClient
może w prosty sposób zostać zastąpione innym rozwiązaniem, np. Fetch API czy Axiosem. Co ważne, odbywa się to w jednym miejscu i bez zmiany publicznego interfejsu HttpClienta
.
Podsumowanie
Tak jak zapowiadałem na początku wpisu, Adapter jest bardzo prostym w implementacji wzorcem projektowym. Mam nadzieję, że udało Ci się dowiedzieć czegoś ciekawego i że wykorzystasz tę wiedzę w praktyce. Zachęcam do zostawienia komentarza oraz do zajrzenia w materiały dodatkowe i źródła.
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić klikając w TEN link. Kupując z tego linku, wspierasz rozwój bloga.
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.