Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

🔥 Zgarnij PŁATNY STAŻ w 3 edycji kursu programowania front end (zapisy do 22.01) 🔥

Wzorzec projektowy Proxy (Pełnomocnik)

Zobacz przykłady i wykorzystaj OOP w projekcie JavaScript

Wyobraź sobie posiadanie własnego pełnomocnika. W moich marzeniach taka osoba szukałaby dla mnie najlepszych promocji. Mogłaby segregować moją pocztę i przekazywać mi tylko najważniejsze maile. Niestety nie znam nikogo, kto ma komfort posiadania takiego pomocnika. Znam za to wielu programistów, którzy korzystają ze swoich Pełnomocników w kodzie.

Spis treści

Pełnomocnik to obiekt, który jest tworzony w miejsce pierwotnego obiektu. Najczęściej wykonuje on „brudną” robotę i angażuje obiekt pierwotny tylko wtedy, gdy jest rzeczywiście potrzebny. Jeśli takie pojęcia jak lazy initialization, pamięć podręczna czy walidacja nie są Ci obce, to na pewno spotkałeś się z Proxy!

 Czym jest Proxy?

Proxy to strukturalny wzorzec projektowy. Wzorce strukturalne wyjaśniają, jak składać obiekty i klasy w większe struktury, zachowując przy tym ich elastyczność i efektywność.

Proxy to obiekt tworzony w miejsce obiektu pierwotnego. Co więcej, powinien on być wykorzystywany zamiast obiektu pierwotnego i rozszerzać go o dodatkowe pola czy metody.

Rozszerzanie skojarzyło Ci się z dziedziczeniem? Słusznie. W tym przypadku Proxy i obiekt pierwotny dziedziczą ten sam interfejs. Oznacza to, że Proxy i obiekt pierwotny mają te same metody – ale mimo to robią różne rzeczy. Proxy jest pośrednikiem między miejscem wywołania akcji a jej wykonania. Wykonuje różne dodatkowe czynności, zanim właściwa akcja zostanie wywołana.

Myślę, że najlepiej będzie posłużyć się przykładem z życia i diagramem UML. Jeśli jeszcze nie wiesz, jak stworzyć diagram i na czym polegają relacje między obiektami, to zachęcam Cię do zapoznania się z moim poprzednim artykułem: „Podstawy UML – diagramy klas”.

 Życiowy przykład wzorca Proxy

Niech naszym obiektem pierwotnym będą drzwi. Drzwi, jak to mają w zwyczaju, służą do otwierania pomieszczenia (metoda open). Jednak gdyby nasze mieszkania nie były wyposażone w zamki na klucze, nie byłyby tak bezpieczne. Dlatego drzwi z zamkiem potraktuję jako jedną całość i nazwę antywłamaniowymi. Będą stanowić Proxy. Robią one to samo, co zwykłe drzwi (implementacja interfejsu DoorInterface), ale dodatkowo korzystają z metody checkKeys w celu sprawdzenia, czy otwieramy mieszkanie odpowiednimi kluczami. Spójrz na diagram ilustrujący ten wzorzec:

diagram przykładu zastosowania wzorca Proxy zgodny ze specyfikacją UML 2.4

Po analizie przykładu wzorzec Proxy powinien być dużo bardziej zrozumiały.

 Kiedy użyć Proxy?

Warto korzystać z Proxy wtedy, gdy nie chcemy od razu wywoływać metody z obiektu pierwotnego. Możesz go użyć np. w takich działaniach jak:

  • Ograniczenie kosztownych operacji przy wykorzystaniu buforowania, czy cache – zamiast ponownie pobierać jakiś plik z API, możesz go zapisać lokalnie i wczytać z pamięci.
  • Kontrola dostępu do miejsca docelowego – czyli uwierzytelnianie użytkownika; zupełnie jak w przypadku naszych drzwi antywłamaniowych z przykładu.
  • Walidacja przesyłanych danych – walidacja poszczególnych pól formularza przed wysłaniem go na serwer.
  • Ukrycie szczegółów implementacyjnych (np. dodatkowych ustawień dla miejsca docelowego), które nie są istotne z perspektywy miejsca wywołania akcji lub niwelują powielanie kodu – jest to np. cachowanie zasobów HTML, CSS czy JavaScript w celu szybszego wczytywania stron internetowych. Odbywa się przy pomocy silnika przeglądarki.

Wzorzec Proxy w JS

 Własne Proxy

Zastosujmy wzorzec w kodzie JavaScript. Posłużę się przedstawioną wcześniej analogią. Na początku stworzę interfejs DoorInterface, który będzie udostępniał metodę open. Jeśli nie znasz interfejsów, zapraszam Cię do zapoznania się z artykułem „4 filary programowania obiektowego”.

export default class DoorInterface {
    open() {
        throw new Error('Not implemented');
    }
}

Teraz stwórzmy oryginalną klasę Door.

import DoorInterface from './interface/DoorInterface.js';

export default class Door extends DoorInterface {
    #isOpened = false;

    open() {
        this.#isOpened = true;
        console.log(`Door is ${this.#isOpened && 'opened 😁'}`);
    }
}

Implementuje ona wcześniej zdefiniowany interfejs. Tworzymy metodę open, która informuje nas, czy udało się otworzyć drzwi.

Zwróć uwagę, że obiekty tego typu mogą już istnieć w Twoim projekcie. Aby kontrolować dostęp do nich, stwórz im pełnomocnika!

Zgodnie z diagramem naszym Proxy ma być klasa AntiBurglarDoor. Wykorzystamy ten sam interfejs, ale dostęp do metody open będzie obwarowany przez metodę checkKeys.

import DoorInterface from './interface/DoorInterface.js';
import Door from './Door.js';

export default class AntiBurglarDoor extends DoorInterface {
    #originalDoor;

    constructor(originalDoor) {
        super();
        if (!(originalDoor instanceof Door)) {
            throw new Error(
                'The original door must be an instance of DoorInterface'
            );
        }

        this.#originalDoor = originalDoor;
    }

    checkKeys() {
        // nie mamy właściwych kluczy
        const hasCorrectKeys = false;
        return hasCorrectKeys;
    }

    open() {
        // jeśli mamy właściwe klucze, to otwórz drzwi
        if (this.checkKeys()) {
            this.#originalDoor.open();
            return;
        }

        console.log('Door closed 😲 You do not have the correct keys');
    }
}

W metodzie checkKeys „na sztywno” zdefiniowaliśmy zmienną hasCorrectKeys z wartością false. W Twoim projekcie powinna znaleźć się tam odpowiednia logika biznesowa, która stanowi istotę pełnomocnika.

Stworzone klasy użyję w głównym pliku projektu – index.js.

import Door from './Door.js';
import AntiBurglarDoor from './AntiBurglarDoor.js';

const door = new Door();
const antiBurglarDoor = new AntiBurglarDoor(door);

// korzystam z oryginalnego obiektu
door.open(); // Door is opened 😁

// korzystam z Proxy
antiBurglarDoor.open(); // Door closed 😲 You do not have the correct keys

Nasz pełnomocnik spełnił swoją funkcję – zablokował dostęp do pomieszczenia, sprawdzając poprawność klucza.

Kod z przedstawionych przykładów możesz podejrzeć w repozytorium.

 Obiekt Proxy wbudowany w JavaScript

Największa do tej pory aktualizacja ECMAScript, czyli ES6, przyniosła ze sobą dużo praktycznych nowości. Jedną z nich jest możliwość korzystania z obiektu Proxy, który obecnie cieszy się niemal całkowitym wsparciem każdej z przeglądarek.

Proxy to nowy konstruktor globalny, który przyjmuje dwa argumenty:

  • obiekt pierwotny,
  • obiekt stanowiący procedurę obsługi, tzw. handler.

Tak jak w przypadku wzorca Proxy, tworzymy obiekt-pomocnika, wykorzystujący obiekt pierwotny. Dzieje się to za pomocą słowa kluczowego new Proxy(). Pomocnik dziedziczy wszystkie metody od obiektu pierwotnego. Aby skorzystać z wbudowanego obiektu Proxy, powinieneś wykorzystać następującą strukturę:

import OriginalObject from './OriginalObject.js';

const originalObject = new OriginalObject();

const originalObjectProxy = new Proxy(originalObject, {
   get(object, key) { },
   set(object, key, newValue) { }
})

Jak widzisz, pierwszym parametrem new Proxy() jest obiekt pierwotny, drugim zaś obiekt handler – zawiera on metody get oraz set zwane też pułapkami (traps). To właśnie za pomocą tych dwóch metod możesz kontrolować pobieranie i ustawianie właściwości w oryginalnym obiekcie. Dokumentacja przedstawia również inne pułapki, z którymi możesz się zapoznać. Ja zostanę przy tych dwóch, gdyż w zupełności mi wystarczą.

Zaprezentuję Ci wykorzystanie obiektu Proxy. Posłużymy się przykładem, w którym odpytamy opensourcowe API. Jego zadaniem jest podawanie informacji o pogodzie. Nasz obiekt pierwotny będzie miał za zadanie:

  • pobierać informację o pogodzie we wskazanej lokalizacji,
  • zwracać pobraną pogodę po użyciu metody showWeather.

Stworzymy również pełnomocnika. Jego rolą będzie kontrola czasu, w którym pobrano informację o pogodzie za pomocą obiektu pierwotnego. Jeśli czas między kolejnymi pobraniami będzie krótszy niż 10 sekund, to zatrzymamy ten proces. Czas mogę zapisywać np. za pomocą zmiennej, którą przechowam w localStorage. Do dzieła!

Aby iść zgodnie z duchem OOP, stworzę interfejs AskWeatherInterface.js:

export default class AskWeatherApiInterface {
    getWeather() {
        throw new Error('Not implemented');
    }
}

Mówi nam on, że klasa potomna musi zaimplementować metodę getWeather.

Klasa-dziecko to WeatherApi.js. Za pomocą fetch oraz async/await pobierzemy informacje o aktualnych warunkach pogodowych:

import AskWeatherApiInterface from './interface/AskWeatherApiInterface.js';

export default class WeatherApi extends AskWeatherApiInterface {
    #latitude;
    #longitude;

    constructor(latitude, longitude) {
        super();
        this.#latitude = latitude;
        this.#longitude = longitude;
    }

    async getWeather() {
        const url = `https://api.open-meteo.com/v1/forecast?latitude=${
            this.#latitude
        }&longitude=${this.#longitude}&current_weather=true`;
        
        const response = await fetch(url);
        const data = await response.json();
        const weather = data.current_weather;

        return weather;
    }
}

Dwa pola prywatne #latitude oraz #longitude określają lokalizację, z której pobierzemy pogodę. Zbudowaliśmy metodę getWeather, która zwraca obiekt weather. Prezentuje się on tak:

JSON z odpowiedzią z API pogodowego

Pozostaje nam stworzyć pomocnika, który będzie pilnował, by requesty do API nie były częstsze niż co 10 sekund. Korzystam z obiektu Proxy i tworzę plik withProxy.js:

const WAIT_TIME = 1000 * 10;

const canCallApiAgain = () => {
    const lastCallTime = localStorage.getItem('callTime');
    if (!lastCallTime) {
        return true;
    }

    const currentTime = new Date().getTime();

    return currentTime - lastCallTime > WAIT_TIME;
};

const logCallTime = () => {
    const currentTime = new Date().getTime();
    localStorage.setItem('callTime', currentTime);
};

const withProxy = weatherApiObject =>
    new Proxy(weatherApiObject, {
        get(object, key) {
            if (key === 'getWeather') {
                if (!canCallApiAgain()) {
                    throw new Error(
                        'You can call API only once per 10 seconds'
                    );
                }

                logCallTime();

                return async function () {
                    const weather = await object[key]();
                    return weather;
                };
            }
        }
    });

export default withProxy;

Funkcje canCallApiAgain oraz logCallTime stanowią funkcje pomocnicze. Pierwsza sprawdza, czy minęło 10 sekund od ostatniego requestu do API, a druga za pomocą klucza callTime zapisuje czas ostatniego calla w localStorage.

Istotą jest funkcja withProxy. Za pomocą new Proxy tworzymy pomocnika obiektu weatherApi. Przekażemy go w parametrze, gdy będziemy pisać kod w głównym pliku index.js. Pułapka get(object, key) sprawdza, czy możemy użyć któregoś z kluczy (key) w oryginalnym obiekcie (object). Jeśli klucz to getWeather – wywołujemy funkcję o tej nazwie. Właśnie w tym miejscu sprawdzam, czy możemy ponownie odpytać API. Jeśli tak, to zapisujemy czas odpytania przez wywołanie logCallTime i prosimy o dane. Jeśli nie, rzucamy błąd.

Zamiast rzucania błędu moglibyśmy również przekazywać ostatnio pobraną odpowiedź. Wykonaj to jako zadanie dodatkowe – to dobry sposób na przećwiczenie Twojej znajomości Proxy!

Przejdźmy do ostatniej części kodu, czyli użycia klasy WeatherApi oraz funkcji withProxy. Tworzę plik index.js:

import WeatherApi from './WeatherApi.js';
import withProxy from './withProxy.js';

const weatherApi = new WeatherApi(52.23, 21.01);

withProxy(weatherApi).getWeather().then(weather => {
    console.log(weather);
});

Gotowe! Stworzyliśmy obiekt odpytujący API o pogodę. Jest on chroniony przez Proxy. Kod przedstawiony powyżej znajdziesz w repozytorium na GitHubie.

 Podsumowanie

Wiesz już, czym jest wzorzec Proxy. Teraz będziesz mógł go wykorzystać, by:

  • ukryć szczegóły związane z implementacją logiki biznesowej,
  • wykonać walidację danych poza obiektem realizującym logikę np. formularza,
  • cachować pobrane zasoby czy kontrolować dostęp do metod odpytujących API.

To tylko kilka przykładów zastosowania pełnomocnika. Może znajdziesz dla niego odpowiednie miejsce w swoim projekcie?

 Źródła

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.

Mam coś dla Ciebie!

W każdy piątek rozsyłam motywujący do nauki programowania newsletter!

Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.

PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.