Wzorzec projektowy adapter - okładka

Wzorzec projektowy Adapter

Data publikacji Kategorie Czysty kod, TypeScript

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 korzystając z TypeScripta przedstawię przykładową implementację.

Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Serdecznie zachęcam do zapoznania się z innymi wpisami z tego cyklu:

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 z spoza organizacji (na przykład z zwewnę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 miejcu gdzie wywołana jest metoda doSomething(). Jednkakże znacznie lepszym rozwiązaniem jest zastąpienie LegacyClass adapterem. Jest to zabezpieczenie na przyszłość w przypadku kolejnej zmiany. 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 parametó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ęać, ż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 wykorzytasz tę wiedzę w praktyce. Zachęcam do zostawienia komentarza oraz do zajrzenia w materiały dodatkowe i źródła.

Źródła i materiały dodatkowe:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.