Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.
Virtual proxy
Strukturalny wzorzec projektowy pełnomocnik opiera się w głównej mierze na posiadaniu spójnego interfejsu z klasą bazową, której dotyczy. Dzięki temu, że pełnomocnik jest swoistego rodzaju pośrednikiem dla logiki danej klasy, to możemy umieścić w nim dodatkowe lub zmienione zachowania względem pierwotnego obiektu.
Istnieje dwa sposoby implementacji tego wzorca: oparty na dziedziczeniu oraz wspólnym interfejsie. Obydwa jednak korzystają wielkimi garściami z podejścia kompozycji zamiast dziedziczenia, o którym już wspominałem w jednym z poprzednich wpisów. Proxy przechowuje w sobie instancję klasy, której pośredniczy i może do niej kierować wszystkie lub część odwołań – zaraz przekonasz się, jakie możliwości daje nam taki zabieg.
Kiedy używać wzorca pełnomocnik
Osobiście, najczęściej stosuję ten wzorzec w przypadku, jeśli mój obiekt jest kosztowny w utworzeniu, a nie zawsze potrzebuję posiadać go w pełni zainicjalizowanego (może to trochę przypominać mechanizm lazy loadingu). Dodajemy wtedy dodatkową abstrakcję w postaci pełnomocnika, który odziedziczy klasę naszego obiektu lub zaimplementuje ten sam interfejs. Pełnomocnik przechowa częściowo zaimplementowany obiekt, a kosztowną operację wykona dopiero, jeśli zajdzie taka potrzeba, tym samym w pełni inicjalizując pierwotny obiekt.
Drugim casem jest sytuacja, kiedy konieczne jest rozszerzenie logiki bazowej o jakieś inne operacje, np. zanim wykonana zostanie metoda validate() trzeba odfiltrować puste rekordy. Zasada jest ta sama, w pełnomocniku realizujemy nowe operacje, a następnie wywołujemy metodę docelową.
Problem do rozwiązania
Wyobraźmy sobie, że tworzymy aplikację do śledzenia naszych aktywności fizycznych, np. biegania czy jazdy na rowerze. Oprócz przechowywania podstawowych informacji takich jak: typ aktywności, data startu, data zakończenia, aplikacja pobiera dodatkowo lokalizację użytkownika. Następnie, na podstawie zebranych odczytów lokalizacji wyliczana jest później pokonana odległość.
class Activity implements ActivityInterface {
constructor(
private readonly id: ActivityId,
private readonly type: ActivityType,
private readonly startedAt: Date,
private trace: TracePoint[] = [],
private finishedAt?: Date,
) {}
getId = (): ActivityId => this.id;
getBaseInfo(): BasicActivityInfo {
return {
type: this.type,
startedAt: this.startedAt,
user: this.user,
finishedAt: this.finishedAt
};
}
addPoint(point: TracePoint) {
this.trace.push(point);
}
getTrace = (): TracePoint[] => this.trace;
getDistance = (): Distance => { /* impl */ }
}
Powiedzmy, że jeden z użytkowników biega dwa razy w tygodniu, po trzydzieści kilometrów, a nasza aplikacja odczytuje lokalizację średnio co pięć metrów. Daje nam to sześć tysięcy obiektów typu TracePoint na jedną tego typu aktywność.
Aktualnie, aby zwrócić podstawowe informacje o aktywności, pobrać wszystkie odczyty lokalizacji czy dodać nowy punkt trasy, musimy załadować pełny obiekt Activity, co nie ma najmniejszego sensu w tym przypadku.
const activity = new Activity(/* data */);
// add 4000 points
const baseInfo = activity.getBaseInfo();
// add 1000 points
const points = activity.getPoints(); // map render
// add 1000 points
const distance = activity.getDistance();
Przykład wzorca pełnomocnik
W celu rozwiązania tego problemu, zaimplementujmy proxy dla klasy aktywności. Implementujemy na nowo wszystkie metody, których zachowania chcemy zmienić. W tym przypadku będą to: getPoints(), getDistance() i addPoint(). Pozostałe kierujemy bezpośrednio do bazowego obiektu.
class ActivityProxy extends Activity {
constructor(
private readonly storage: ActivityStorage,
private readonly activity: Activity,
private initialized = false
) {}
getId = (): ActivityId => this.activity.getId();
getBaseInfo = (): BasicActivityInfo =>
this.activity.getBaseInfo();
getPoints = (): TracePoint[] =>
return this.storage.getPoints(this.getId());
getDistance(): Distance {
if (!this.initialized) {
this.initialize();
}
return this.activity.getDistance();
}
addPoint(point: TracePoint) {
this.storage.addPoint(trace);
if (initialized) {
this.activity.addPoint(point);
}
}
private initialize() {
for (const point of this.getPoints()) {
this.addPoint(point);
}
this.initialized = true;
}
}
Aktualnie punkty trasy trzymamy w pewnym storage’u, który aktualizujemy po wywołaniu addPoint(), a również w aktywności, jeżeli ta została zainicjalizowana. Dzięki temu obiekt aktywności jest w pełni ładowany dopiero wtedy, kiedy chcemy wyliczyć dystans.
Podsumowanie
- Pełnomocnik to obiekt zastępczy delegujący zadania do swojego klienta.
- Dzięki zastosowaniu dodatkowej klasy możemy rozszerzyć bazową logikę o dodatkowe operacje.
- Wzorzec pełnomocnik jest przydatny, kiedy musimy inicjalizować duże, kosztowne obiekty.
Dodaj komentarz