Proxy

Wzorce strukturalne

ukończona

1. Definicja

Proxy (pol. pełnomocnik, zastępca) to strukturalny wzorzec projektowy, który dostarcza obiekt zastępczy (surogat) kontrolujący dostęp do innego obiektu.

Provide a surrogate or placeholder for another object to control access to it. — Gang of Four, Design Patterns

Najprościej rzecz ujmując: Proxy udaje prawdziwy obiekt, ale w rzeczywistości pośredniczy w dostępie do niego.

Klient wywołuje metody na obiekcie Proxy, myśląc, że rozmawia bezpośrednio z właściwym obiektem (tzw. Real Subject). Tymczasem Proxy może przed przekazaniem wywołania dalej albo zamiast tego wykonać dodatkowe czynności: sprawdzić uprawnienia, opóźnić kosztowną operację, zalogować dostęp czy skomunikować się z odległym serwerem.

Kluczowe jest to, że Proxy implementuje ten sam interfejs co obiekt, który reprezentuje. Dzięki temu klient nie musi wiedzieć, czy trzyma w ręku prawdziwy obiekt, czy jego zastępcę.


2. Przykład z życia: zobaczmy jak to zrozumieć?

Wyobraźmy sobie, że chcemy kupić działkę budowlaną, ale mieszkamy w innym mieście i nie mamy czasu jeździć do notariusza, urzędu gminy czy na oględziny terenu.

Co robimy? Ustanawiamy pełnomocnika, najczęściej prawnika, który działa w naszym imieniu. Dla urzędnika, notariusza czy sprzedającego pełnomocnik wygląda i zachowuje się jak my sami: podpisuje dokumenty, zadaje pytania, reprezentuje nasze interesy. Formalnie te same czynności, które my byśmy wykonali osobiście, wykonuje ktoś inny w naszym zastępstwie.

Ale pełnomocnik nie jest jedynie "kopią" nas samych. On dokłada coś od siebie:

  • sprawdza, czy transakcja ma sens prawny, zanim w ogóle zaangażuje naszą stronę (kontrola dostępu),
  • jedzie na oględziny działki tylko wtedy, gdy faktycznie mamy poważny zamiar zakupu, nie fatyguje się bez potrzeby (leniwe, opóźnione wykonanie kosztownej czynności),
  • prowadzi rejestr wszystkich czynności podjętych w naszym imieniu (logowanie).

Gdybyśmy chcieli działać bez pełnomocnika, sami musielibyśmy jeździć do notariusza, sami sprawdzać zapisy w księgach wieczystych, sami pilnować terminów. Pełnomocnik "podszywa się" pod nas na tyle skutecznie, że otoczenie w ogóle nie musi wiedzieć, że nie działamy osobiście.

Ćwiczenie myślowe: wyobraźmy sobie teraz, że zamiast pełnomocnika-prawnika mamy do czynienia z obiektem w kodzie — RealEstateService, który wykonuje kosztowną operację: łączy się z rejestrem ksiąg wieczystych, pobiera dane o działce i sprawdza obciążenia hipoteczne. To połączenie trwa kilka sekund i kosztuje pieniądze (opłata za zapytanie do rejestru).

Co jeśli nie każdy użytkownik naszej aplikacji powinien mieć prawo wykonać to zapytanie? Co jeśli chcemy wykonywać je dopiero wtedy, gdy jest naprawdę potrzebne, a nie przy każdym wejściu na stronę oferty?

Dokładnie w tym miejscu w kodzie pojawia się potrzeba Proxy.


3. Przykład - naruszenie (brak Proxy)

Załóżmy, że tworzymy system sklepu internetowego z modułem ofert nieruchomości. Mamy klasę RealEstateService, która przy każdym wywołaniu getPropertyDetails() łączy się z zewnętrznym rejestrem ksiąg wieczystych, co jest kosztowne i czasochłonne.

interface RealEstateService {
    PropertyDetails getPropertyDetails(String propertyId);
}

class RealRealEstateService implements RealEstateService {

    @Override
    public PropertyDetails getPropertyDetails(String propertyId) {
        // symulacja kosztownego połączenia z rejestrem ksiąg wieczystych
        System.out.println("Łączenie z rejestrem ksiąg wieczystych...");
        simulateNetworkDelay();
        return fetchFromRegistry(propertyId);
    }

    private void simulateNetworkDelay() {
        try {
            Thread.sleep(2000); // 2 sekundy opóźnienia sieciowego
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private PropertyDetails fetchFromRegistry(String propertyId) {
        return new PropertyDetails(propertyId, "Działka budowlana, 1200 m²", true);
    }
}

A klient (np. kontroler w naszej aplikacji) korzysta z niej tak:

class PropertyController {

    private final RealEstateService realEstateService = new RealRealEstateService();

    public void showPropertyPage(String propertyId, User user) {
        // BRAK jakiejkolwiek kontroli dostępu!
        // BRAK cache'owania - każde wejście na stronę = nowe zapytanie do rejestru!
        PropertyDetails details = realEstateService.getPropertyDetails(propertyId);
        render(details);
    }
}

Co jest złe w tym podejściu?

  • Brak kontroli dostępu — każdy użytkownik, niezależnie od uprawnień, może wywołać kosztowne zapytanie do rejestru. Nie sprawdzamy, czy jest zalogowany, czy ma wykupiony dostęp do szczegółowych danych.
  • Brak cache'owania — jeśli sto osób w ciągu godziny obejrzy tę samą ofertę, wykonamy sto identycznych, kosztownych zapytań do zewnętrznego rejestru.
  • Brak logowania — nie wiemy, kto i kiedy sprawdzał dane konkretnej działki, a to może być istotne z punktu widzenia audytu.
  • Mieszanie odpowiedzialności — gdybyśmy chcieli dodać kontrolę dostępu czy cache, musielibyśmy modyfikować RealRealEstateService, czyli klasę, która powinna zajmować się wyłącznie pobieraniem danych z rejestru, a nie decydowaniem, kto ma do nich prawo.
  • Trudność w testowaniu — nie możemy łatwo przetestować logiki kontrolera bez wywołania prawdziwego, kosztownego połączenia sieciowego.

4. Przykład - zgodny z Proxy

Wprowadzamy klasę RealEstateServiceProxy, która implementuje ten sam interfejs RealEstateService, ale przed przekazaniem wywołania do prawdziwego serwisu dokłada kontrolę dostępu i cache.

class RealEstateServiceProxy implements RealEstateService {

    private final RealEstateService realService;
    private final Map<String, PropertyDetails> cache = new HashMap<>();
    private final User currentUser;

    public RealEstateServiceProxy(User currentUser) {
        this.realService = new RealRealEstateService(); // leniwa referencja do prawdziwego obiektu
        this.currentUser = currentUser;
    }

    @Override
    public PropertyDetails getPropertyDetails(String propertyId) {

        // 1. Kontrola dostępu - zanim cokolwiek zrobimy, sprawdzamy uprawnienia
        if (!currentUser.hasAccessToDetailedOffers()) {
            throw new AccessDeniedException(
                "Brak uprawnień do szczegółowych danych działki: " + propertyId);
        }

        // 2. Cache - jeśli dane już mamy, nie łączymy się ponownie z rejestrem
        if (cache.containsKey(propertyId)) {
            System.out.println("Zwracam dane z cache dla: " + propertyId);
            return cache.get(propertyId);
        }

        // 3. Logowanie - rejestrujemy, kto i kiedy sprawdzał dane
        System.out.println("Użytkownik " + currentUser.getName()
            + " sprawdza działkę " + propertyId + " o " + LocalDateTime.now());

        // 4. Dopiero teraz delegujemy do prawdziwego, kosztownego obiektu
        PropertyDetails details = realService.getPropertyDetails(propertyId);
        cache.put(propertyId, details);

        return details;
    }
}

Kontroler nie musi się zmieniać ani wiedzieć, że rozmawia z zastępcą, a nie z prawdziwym serwisem:

class PropertyController {

    public void showPropertyPage(String propertyId, User user) {
        // Kontroler wciąż operuje na interfejsie RealEstateService.
        // Nie wie i nie musi wiedzieć, że w rzeczywistości dostał Proxy.
        RealEstateService service = new RealEstateServiceProxy(user);
        PropertyDetails details = service.getPropertyDetails(propertyId);
        render(details);
    }
}

Co się zmieniło, krok po kroku?

  1. Wspólny interfejs — RealEstateServiceProxy i RealRealEstateService implementują to samo RealEstateService, więc klient może podać jedno miejsce w kodzie i nie wie, którą implementację faktycznie dostał.
  2. Kontrola dostępu przeniesiona do Proxy — logika sprawdzania uprawnień nie zaśmieca już klasy odpowiedzialnej wyłącznie za pobieranie danych z rejestru.
  3. Cache jako naturalne rozszerzenie — Proxy przechowuje już pobrane dane, więc kosztowne połączenie sieciowe wykonujemy tylko raz na działkę, a nie przy każdym wejściu na stronę.
  4. Leniwe tworzenie prawdziwego obiektu — moglibyśmy pójść o krok dalej i tworzyć RealRealEstateService dopiero w momencie pierwszego realnego zapytania (tzw. lazy initialization), zamiast robić to z góry.
  5. Łatwiejsze testowanie — możemy podstawić pod RealEstateService atrapę (mocka) i przetestować logikę kontrolera bez angażowania prawdziwego, kosztownego serwisu.

Porównanie "przed / po": wcześniej kontroler był bezpośrednio uzależniony od kosztownej, niechronionej implementacji. Teraz kontroler zależy jedynie od interfejsu, a cała odpowiedzialność za ochronę i optymalizację dostępu spoczywa na Proxy — zgodnie zresztą również z zasadą pojedynczej odpowiedzialności (SRP).


5. Diagram struktury

┌───────────────────────┐
│  <<interface>>        │
│  RealEstateService    │
├───────────────────────┤
│ + getPropertyDetails()│
└─────────▲─────────────┘
          │ implements
   ┌──────┴──────────────────────────┐
   │                                 │
┌──┴─────────────────────┐    ┌──────┴───────────────────┐
│ RealEstateServiceProxy │    │ RealRealEstateService    │
├────────────────────────┤    ├──────────────────────────┤
│ - realService          │──▶ │ + getPropertyDetails()   │
│ - cache                │    └──────────────────────────┘
│ - currentUser          │
├────────────────────────┤
│ + getPropertyDetails() │
│   (kontrola dostępu,   │
│    cache, logowanie,   │
│    delegacja)          │
└───────────▲────────────┘
            │ używa (przez interfejs)
      ┌─────┴───────┐
      │ Klient      │
      │ (Controller)│
      └─────────────┘

Klient rozmawia wyłącznie z interfejsem RealEstateService. Proxy stoi "przed" prawdziwym obiektem i decyduje, kiedy (i czy w ogóle) przekazać wywołanie dalej.


6. Dlaczego Proxy jest ważne?

Oddzielenie odpowiedzialności — logika kontroli dostępu, cache'owania czy logowania nie zaśmieca klasy odpowiedzialnej za właściwą logikę biznesową (np. komunikację z rejestrem).

Kontrola bez modyfikacji klienta — klient (np. kontroler) nie musi wiedzieć, że korzysta z Proxy zamiast prawdziwego obiektu — wszystko dzieje się za wspólnym interfejsem.

Optymalizacja kosztownych operacji — Proxy pozwala odłożyć w czasie (lub całkowicie uniknąć) tworzenia i wywoływania drogich zasobów: połączeń sieciowych, dużych plików, zewnętrznych usług.

Bezpieczeństwo — Proxy to naturalne miejsce do umieszczenia kontroli uprawnień, walidacji czy audytu, bez ryzyka pominięcia tych kroków w innym miejscu kodu.

Zaniedbanie tego wzorca w dużych projektach zwykle objawia się rozproszoną logiką kontroli dostępu i cache'owania po całej bazie kodu — te same sprawdzenia powtarzane w dziesiątkach kontrolerów, zamiast w jednym, dobrze przetestowanym miejscu.


7. Kiedy stosować?

Proxy sprawdza się szczególnie dobrze w kilku klasycznych sytuacjach:

  • Virtual Proxy — gdy tworzenie prawdziwego obiektu jest kosztowne, a chcemy je odłożyć w czasie (np. leniwe ładowanie dużych obrazów produktów w sklepie internetowym).
  • Protection Proxy — gdy chcemy kontrolować, kto ma prawo wywołać daną operację (jak w naszym przykładzie z dostępem do szczegółowych danych działki).
  • Remote Proxy — gdy prawdziwy obiekt znajduje się na innym serwerze, a Proxy ukrywa szczegóły komunikacji sieciowej (np. RMI, gRPC stuby).
  • Caching Proxy — gdy chcemy przechowywać wyniki kosztownych operacji, by nie wykonywać ich wielokrotnie.
  • Logging/Auditing Proxy — gdy chcemy rejestrować każde wywołanie danej operacji bez zaśmiecania logiki biznesowej.

Przykłady z frameworków:

  • Spring AOP opiera się w całości na mechanizmie Proxy — adnotacje takie jak @Transactional czy @Cacheable działają właśnie dzięki temu, że Spring w czasie działania aplikacji tworzy dynamiczny obiekt Proxy "owijający" nasz bean.
  • Hibernate wykorzystuje Proxy przy leniwym ładowaniu (lazy loading) powiązanych encji — gdy pobieramy encję z relacją @ManyToOne(fetch = FetchType.LAZY), w rzeczywistości dostajemy Proxy, który dociąga dane z bazy dopiero przy pierwszym odwołaniu.
  • Java Dynamic Proxy (java.lang.reflect.Proxy) oraz RMI to standardowe mechanizmy tworzenia proxy w czystej Javie, bez frameworków.

Warto sięgnąć po Proxy zwłaszcza wtedy, gdy zauważamy, że ta sama logika kontroli dostępu, cache'owania lub logowania powtarza się w wielu miejscach — to sygnał, że powinna zostać scentralizowana w jednym, dobrze zdefiniowanym zastępcy.

"Pełnomocnik przed drzwiami" — Proxy stoi przed prawdziwym obiektem i decyduje, czy, kiedy i jak przekazać wywołanie dalej.