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:
- Wzorzec projektowy Proxy
- Wzorzec projektowy Factory (Fabryka)
- Wzorzec projektowy Singleton
- Wzorzec projektowy Command (Polecenie)
- Wzorzec projektowy Fasada
- Wzorzec projektowy Prototyp
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 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:
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:
- https://www.samouczekprogramisty.pl/wzorzec-projektowy-adapter
- https://www.ezzylearning.net/tutorial/adapter-design-pattern-in-asp-net-core
- https://sourcemaking.com/design_patterns/adapter
- https://refactoring.guru/pl/design-patterns/adapter
- https://brasil.cel.agh.edu.pl/~09sbfraczek/adapter%2C1%2C32.html
Zapisz się na mailing
Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.