Wzorzec pełnomocnik (virtual proxy)

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.
Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również