Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do Discorda!
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.
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!
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”.
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:
Po analizie przykładu wzorzec Proxy powinien być dużo bardziej zrozumiały.
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:
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.
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:
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:
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}¤t_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:
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.
Wiesz już, czym jest wzorzec Proxy. Teraz będziesz mógł go wykorzystać, by:
To tylko kilka przykładów zastosowania pełnomocnika. Może znajdziesz dla niego odpowiednie miejsce w swoim projekcie?
Udostępnij ten artykuł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
Chcesz zostać (lepszym) programistą i lepiej zarabiać?
🚀 Porozmawiajmy o nauce programowania, poszukiwaniu pracy, o rozwoju kariery lub przyszłości branży IT!
Umów się na ✅ bezpłatną i niezobowiązującą rozmowę ze mną.
Chętnie porozmawiam o Twojej przyszłości i pomogę Ci osiągnąć Twoje cele! 🎯