Proxy Pattern w C# — anatomia GoF i kontrola dostępu

Kolejny wzorzec z serii opartej na Anatomii wzorca — 8 elementach opisu GoF. Tym razem Proxy — obiekt-zastępnik, który staje między klientem a właściwym obiektem, żeby kontrolować dostęp do niego: opóźnić kosztowną inicjalizację, dodać cache albo sprawdzić uprawnienia.
1. Intent (Zamierzenie)
Dostarcza zastępnik lub pełnomocnika innego obiektu, aby kontrolować dostęp do niego.
Krótko: klient myśli, że rozmawia z prawdziwym obiektem, a rozmawia z proxy o tym samym interfejsie — a proxy decyduje, kiedy i jak przekazać wywołanie dalej.
2. Also Known As (Inne nazwy)
Surrogate (namiastka, pełnomocnik) — bo proxy występuje w imieniu prawdziwego obiektu.
3. Motivation (Motywacja)
GoF opisuje edytor dokumentów, który nie chce ładować wszystkich osadzonych obrazów przy otwarciu pliku — kosztownych w pamięci i czasie. Zamiast prawdziwych obrazów wstawia ich proxy, które ładują obraz dopiero, gdy trzeba go narysować.
W .NET znasz ten wzorzec z codziennej pracy: Entity Framework Core generuje proxy encji do leniwego ładowania powiązań (kolekcja dogrywana z bazy dopiero przy pierwszym dostępie), a proxy cache’ujące zwraca zapamiętany wynik zamiast ponownie odpytywać wolne API.
4. Applicability (Kiedy stosować)
Stosuj Proxy, gdy potrzebujesz pośrednika bogatszego niż zwykła referencja:
- virtual proxy — odracza kosztowną inicjalizację, dopóki obiekt nie jest naprawdę potrzebny,
- protection proxy — kontroluje dostęp na podstawie uprawnień,
- caching proxy — zapamiętuje wyniki kosztownych operacji,
- remote proxy — reprezentuje lokalnie obiekt żyjący w innym procesie/na innej maszynie.
5. Structure (Struktura)
┌──────────┐ ┌──────────────────┐
│ Client │ ─────> │ <<interface>> │
└──────────┘ │ Subject │
│──────────────────│
│ +Request() │
└──────────────────┘
△ △
┌──────────┘ └──────────┐
┌──────────────┐ ┌──────────────────┐
│ RealSubject │ <─ ─ ─ ─ ─ │ Proxy │
│──────────────│ przekazuje │───────────────────│
│ +Request() │ │ -realSubject │
└──────────────┘ │ +Request() │
└──────────────────┘Proxy i RealSubject dzielą ten sam interfejs Subject, więc klient nie odróżnia jednego od drugiego. Proxy trzyma referencję do RealSubject i przekazuje mu wywołania, gdy uzna to za stosowne.
6. Participants (Uczestnicy)
- Subject — wspólny interfejs RealSubject i Proxy; dzięki niemu proxy jest wymienne z prawdziwym obiektem.
- RealSubject — właściwy obiekt, do którego proxy kontroluje dostęp.
- Proxy — trzyma referencję do RealSubject, implementuje ten sam interfejs i steruje dostępem (leniwie tworzy, cache’uje, sprawdza uprawnienia).
7. Collaborations (Współpraca)
Proxy przekazuje żądania do RealSubject wtedy i tak, jak wynika to z jego rodzaju — np. dopiero po utworzeniu obiektu (virtual) albo po sprawdzeniu uprawnień (protection).
Klient operuje na interfejsie Subject, nie wiedząc, czy trafił na proxy, czy na prawdziwy obiekt.
8. Consequences (Konsekwencje)
Plus: dodaje kontrolę dostępu (leniwość, cache, uprawnienia) bez zmiany RealSubject ani kodu klienta. Plus: RealSubject i klient nic o proxy nie wiedzą. Minus: dodatkowa warstwa pośrednictwa może wprowadzić opóźnienie i komplikację; przy remote proxy dochodzi zawodność sieci ukryta za zwykłym wywołaniem metody.
—
Proxy w C# — od diagramu do kodu
### Scenariusz
Serwis pogodowy odpytuje wolne, płatne API. Ten sam kod pyta o pogodę wielokrotnie w krótkim czasie — chcemy cache’ować wynik, nie zmieniając ani serwisu, ani kodu klienta.
### Subject i RealSubject
public interface IPogodaService
{
string Pobierz(string miasto);
}
public class ApiPogodaService : IPogodaService // RealSubject — kosztowny
{
public string Pobierz(string miasto)
{
Console.WriteLine($"[API] realne zapytanie o {miasto}...");
Thread.Sleep(1000); // symulacja wolnego API
return $"Pogoda dla {miasto}: 21°C";
}
}### Caching Proxy
public class CachingPogodaProxy : IPogodaService
{
private readonly IPogodaService _real;
private readonly Dictionary<string, string> _cache = new();
public CachingPogodaProxy(IPogodaService real) => _real = real;
public string Pobierz(string miasto)
{
if (_cache.TryGetValue(miasto, out var zapamietane))
return zapamietane; // trafienie w cache — bez zapytania do API
var wynik = _real.Pobierz(miasto); // przekazanie do RealSubject
_cache[miasto] = wynik;
return wynik;
}
}IPogodaService pogoda = new CachingPogodaProxy(new ApiPogodaService());
pogoda.Pobierz("Kraków"); // [API] realne zapytanie... (1 s)
pogoda.Pobierz("Kraków"); // natychmiast z cache, bez APIKlient wciąż widzi IPogodaService — nie wie, że między nim a API stanął cache. ApiPogodaService też nie wie o istnieniu proxy.
Idiom C# — Lazy<T> jako wbudowany virtual proxy
Dla najczęstszego zastosowania (leniwe tworzenie kosztownego obiektu) C# ma gotowy virtual proxy — Lazy<T>:
private readonly Lazy<PolaczenieZBaza> _polaczenie =
new(() => new PolaczenieZBaza()); // utworzone dopiero przy pierwszym .Value
public void Uzyj() => _polaczenie.Value.Wykonaj(); // tu następuje inicjalizacjaLazy<T> to Proxy „z pudełka”: obiekt powstaje przy pierwszym dostępie do .Value, nie wcześniej. Leniwe ładowanie relacji w EF Core działa na tej samej idei — tylko proxy generuje framework.
Pułapka: Proxy to nie Decorator
Proxy i Decorator mają tę samą strukturę (owijają obiekt tego samego interfejsu) — i to częste pytanie na rozmowie. Różni je cel:
- Decorator — *dodaje zachowanie* do obiektu (np. kompresję, szyfrowanie); zwykle owijasz wielokrotnie, składając funkcje.
- Proxy — *kontroluje dostęp* do obiektu (leniwość, cache, uprawnienia); zwykle jeden, zarządza cyklem życia lub tożsamością RealSubject.
Ta sama forma, inna intencja: „co ten obiekt robi z wywołaniem” — wzbogaca je (Decorator) czy reguluje dostęp do oryginału (Proxy).
Wariant: rodzaje proxy
Powyższy przykład to caching proxy. Ta sama struktura obsłuży protection proxy (sprawdź uprawnienia przed _real.Request()), virtual proxy (utwórz _real dopiero przy pierwszym wywołaniu) i remote proxy (przekaż wywołanie przez sieć). Kod szkieletu jest identyczny — różni się tylko to, *co* proxy robi, zanim (lub czy w ogóle) przekaże żądanie dalej.
Podsumowanie
Proxy stawia między klientem a obiektem zastępnik o tym samym interfejsie, który kontroluje dostęp: odracza kosztowną inicjalizację, cache’uje wyniki, sprawdza uprawnienia albo reprezentuje obiekt zdalny — a robi to bez zmiany prawdziwego obiektu i kodu klienta. W C# najczęstszy wariant (leniwość) masz gotowy jako Lazy<T>, a leniwe ładowanie EF Core to Proxy generowany przez framework. Od Decoratora odróżnia go intencja: Decorator dodaje zachowanie, Proxy reguluje dostęp — mimo identycznej struktury.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →