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




Polecane wpisy:

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec strategia (strategy pattern)

Jeżeli masz dość ifologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.