Wzorzec projektowy Proxy - okładka

Wzorzec projektowy Proxy

Ten wpis jest kolejnym wpisem z serii wpisów o wzorcach projektowych. Po przeczytaniu tego wpisu, zachęcam Cię do zapoznania się z innymi wpisami z tej serii:

Tym razem omówię strukturalny wzorzec projektowy Proxy (Pełnomocnik).

Proxy jako serwer pośredniczący

Często pierwszym skojarzeniem ze słowem proxy jest serwer proxy. Jest to skojarzenie jak najbardziej słuszne.  Ideą proxy jest pośredniczenie między miejscem wywołania akcji, np. zapytania a miejscem docelowym, w którym dana akcja ma coś wykonać. Przykładowymi zastosowaniami proxy są:

  • Odciążenie serwera docelowego poprzez buforowanie lub cache’owanie żądań.
  • Odizolowanie serwera docelowego – może zaistnieć taki przypadek, gdy ruch do serwera docelowego jest kierowany całkowicie poprzez serwer proxy.
  • Filtrowanie czy modyfikacja danych wejściowych.

Mechanizm Proxy w JavaScript

Wraz z nadejściem standardu ES6, do języka JavaScript został dodany mechanizm proxy w postaci klasy Proxy. Idea użycia tej klasy jest taka sama jak w postaci serwera pośredniczącego. Klasa Proxy pozwala na ustanowienie pełnomocnika między programistą a obiektem docelowym. Przy tworzeniu nowej instancji klasy Proxy należy przekazać obiekt docelowy oraz handler, który odpowiada za obsługę logiki w samym Proxy. Przykład kodu wykorzystującego JS’owe proxy przedstawiam na poniższym fragmencie kodu:


const user = {
    firstName: 'Foo',
    lastName: 'Bar',
    email: 'foo.bar@example.com',
    password: 'superSecretPassword'
}

const handler = {
    secretProperties: [ 'password' ],
  
    get( target, property ) {
        return this.secretProperties.includes( property ) ? undefined : target[ property ];
    }
}

const proxyUser = new Proxy( user, handler );

console.log( proxyUser.password ); // undefined
console.log( proxyUser.email ); // foo.bar@example.com

Przedstawiony fragment kodu przy próbie odczytu ukrywa wartości zdefiniowane w tablicy secretProperties. Dzięki temu, w przypadku przedstawionego kodu próba odczytu właściwości password zakończy się niepowodzeniem. Zamiast hasła zostanie zwrócona wartość undefined. Oczywiście, przedstawiony przykład jest czysto edukacyjny i nie ma on zastosowania produkcyjnego. Ma on za zadanie jedynie przedstawić sposób działania Proxy w JS. Analogicznie do przedstawionego przykładu można by wykorzystać metodę set, na przykład do walidacji adresu email czy siły hasła podczas zmiany wartości. Jednym z popularnych rozwiązań wykorzystujących mechanizm JS’owych Proxy jest MobX. Informację tę można znaleźć w oficjalnej dokumentacji projektu.

Wzorzec projektowy Proxy

Mając już podstawowe pojęcie na czym polega idea proxy, można przejść do zapoznania się z wzorcem projektowym Proxy. Analogicznie do zastosowań serwera pośredniczącego, tak i wzorzec projektowy Proxy pozwala na rozwiązanie kilku problemów:

  • W przypadku tworzenia kosztownych obiektów, pozwala na ograniczenie tego zachowania lub wdrożenie mechanizmu buforowania lub cache.
  • Kontrola dostępu do miejsca docelowego.
  • Ukrycie dodatkowych szczegółów implementacyjnych związanych z dostępem do zasobu.

Do przedstawienia przykładowego wzorca projektowego Proxy posłuży mi poniższy fragment kodu:


interface IDataRepository {
    get(): ISecretData;
}

interface ISecretData {
    secretValue: string;
}

interface IAuthenticationServer {
    authenticate( request: Record<string,unknown> ): Promise<boolean>;
}

class SomeService {
    public constructor(
        private readonly _dataRepository: IDataRepository
    ) {}
    public async getData(
        requester: Record<string,unknown>,
        authenticationProxy: IAuthenticationServer
    ): Promise<ISecretData> {
        const isAuthenticated: boolean = await authenticationProxy.authenticate( requester );

        if ( !isAuthenticated ) {
            throw new Error( 'Access Denied. Requester is not authenticated!' );
        }

        return this._dataRepository.get();
    }
}

class AuthenticationProxy implements IAuthenticationServer {
    public constructor(
        private readonly _authenticationServer: IAuthenticationServer
    ) {}

    public async authenticate( requestData: Record<string,unknown> ): Promise<boolean> {
        const request: Record<string,unknown> = {
            destination: createDestination( 'some-destination' ),
            body: prepareRequestBody( requestData.accessToken ),
            headers: prepareRequestHeaders()
        }

        return this._authenticationServer.authenticate( request );
    }
}

Pierwszym elementem, na który warto zwrócić uwagę jest identyczny interfejs w przypadku AuthenticationProxy jak i AuthenticationServer. Obydwie klasy spełniają wspólny interfejs IAuthenticationServer.

W przedstawionym przypadku proxy jest odpowiedzialne za poprawne przygotowanie zapytania do serwera uwierzytelniającego. Klasa AuthenticationProxy przygotowuje adres docelowy, dodaje wymagane nagłówki i przygotowuje ciało zapytania, w rezultacie zwraca informację prawda/fałsz czy token przekazany przez requestera jest poprawny. Dzięki wykorzystaniu proxy, programista odpowiedzialny za kod klasy SomeService nie jest zmuszony znać szczegółów implementacyjnych związanych z komunikacją z serwerem uwierzytelniającego. Ponadto, sam kod logiki biznesowej nie zawiera żadnego kodu niepowiązanego z tą logiką. Dzięki temu kod staje się znacznie prostszy, bardziej czytelny, prostszy w rozbudowie i utrzymaniu.

W dalszej rozbudowie przedstawionego kodu nic nie stoi na przeszkodzie, aby w klasie AuthenticationProxy została dodana logika związana z cache’owaniem rezultatów żądań. Przykładowo, jeśli dwa żądania z takim samym tokenem zostały wysłane w bardzo krótkim odstępie czasu, to istnieje znikome prawdopobieństwo, że token stracił swoją ważność w tym czasie. Wykorzystując proxy można ograniczyć ilość zapytań do serwera uwierzytelniającego i zwrócić odpowiedź przechowywaną w cache. Oczywiście, wszystko zależy od specyfiki aplikacji. Z pewnośćią znajdą się aplikacje, gdzie zastosowanie takiego uproszczenia byłoby krytycznym błędem bezpieczeństwa.

Podsumowanie

Mam nadzieję, że po lekturze tego artykułu idea proxy oraz wzorzec projektowy Proxy nie będą Ci obce! Zachęcam do zostawienia oceny, komentarza oraz zapoznania się ze źródłami i materiałami dodatkowymi.

Źródła i materiały dodatkowe