Wzorzec projektowy adapter - okładka

Wzorzec projektowy Adapter

Opublikowano Kategorie Czysty kod, TypeScriptCzas czytania 4min

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.

 

wzorzec Adapter - diagram 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.

Implementacja adapter w kliencie HTTP

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.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

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.

Subscribe
Notify of
guest

0 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments