🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Decorator Pattern w C# – przykład z System.IO.Stream

Kolejny wzorzec z serii opartej na Anatomii wzorca — 8 elementach opisu GoF. Tym razem Decorator — wzorzec, którego prawdopodobnie już używałeś, nawet o tym nie wiedząc, jeśli kiedykolwiek otwierałeś plik przez FileStream.

1. Intent (Zamierzenie)

Dołącza dodatkowe obowiązki do obiektu dynamicznie. Decorator to elastyczna alternatywa dla dziedziczenia jako sposobu rozszerzania funkcjonalności.

Krótko: zamiast tworzyć podklasę na każdą kombinację dodatkowych cech, owijasz obiekt innym obiektem o tym samym interfejsie, który dokłada zachowanie przed lub po przekazaniu wywołania dalej.

2. Also Known As (Inne nazwy)

Wrapper — Decorator dosłownie “owija” (wraps) opakowywany obiekt, zachowując ten sam interfejs na zewnątrz.

3. Motivation (Motywacja)

GoF opisuje TextView w bibliotece GUI, który czasem potrzebuje obramowania, czasem paska przewijania, czasem obu naraz. Rozwiązanie przez dziedziczenie wymagałoby klasy na każdą kombinację: BorderedTextViewScrollableTextViewBorderedScrollableTextView — i eksplozji podklas przy każdej nowej cesze.

Decorator rozwiązuje to inaczej: BorderDecorator i ScrollDecorator owijają dowolny TextView, dokładając swoją cechę. Możesz je składać w dowolnej kombinacji w runtime, bez tworzenia ani jednej nowej klasy na każdą kombinację.

4. Applicability (Kiedy stosować)

Stosuj Decorator gdy:

  • chcesz dodać obowiązki do pojedynczych obiektów dynamicznie i przezroczyście, bez wpływu na inne obiekty tej samej klasy,
  • obowiązek powinien dać się później zdjąć,
  • rozszerzanie przez dziedziczenie jest niepraktyczne — zbyt wiele niezależnych cech dałoby eksplozję podklas, albo klasa bazowa w ogóle nie jest dostępna do podklasowania.

Sygnał ostrzegawczy w kodzie: rosnąca liczba podklas różniących się tylko kombinacją włączonych/wyłączonych cech (KawaZMlekiemKawaZCukremKawaZMlekiemICukrem…).

5. Structure (Struktura)

┌──────────────────┐
│  <<interface>>   │
│    Component     │
│──────────────────│
│ +Operation()     │
└──────────────────┘
         △
   ┌─────┴──────────────────┐
┌──────────────────┐  ┌──────────────────┐
│ ConcreteComponent│  │     Decorator    │
│──────────────────│  │──────────────────│
│ +Operation()     │  │ -component       │
└──────────────────┘  │ +Operation()     │
                      └──────────────────┘
                                 △
                       ┌─────────┴─────────┐
             ┌──────────────────┐ ┌──────────────────┐
             │ConcreteDecoratorA│ │ConcreteDecoratorB│
             │──────────────────│ │──────────────────│
             │+Operation()      │ │ +Operation()     │
             └──────────────────┘ └──────────────────┘

Decorator implementuje ten sam interfejs co Component i jednocześnie trzyma referencję do owijanego Component. Wywołanie Operation() na dekoratorze robi coś dodatkowego, a potem (albo najpierw) deleguje do owiniętego obiektu.

6. Participants (Uczestnicy)

  • Component — wspólny interfejs dla obiektów, które można dekorować.
  • ConcreteComponent — podstawowy obiekt, do którego dokładane są dodatkowe obowiązki.
  • Decorator — trzyma referencję do Component i implementuje ten sam interfejs.
  • ConcreteDecorator — dokłada konkretny obowiązek przed/po wywołaniu owiniętego obiektu.

7. Collaborations (Współpraca)

Decorator przekazuje żądania do swojego obiektu Component. Może wykonać dodatkowe operacje przed lub po przekazaniu żądania.

Kluczowe: Decorator nie zastępuje Component — deleguje do niego, dokładając własne zachowanie na wejściu lub wyjściu.

8. Consequences (Konsekwencje)

Plus: więcej elastyczności niż statyczne dziedziczenie — obowiązki można dodawać i zdejmować w runtime. Plus: unika przeładowanych klas wysoko w hierarchii — zamiast tego budujesz prosty rdzeń i doklejasz małe dekoratory. Minus: system złożony z wielu małych, podobnych do siebie obiektów jest trudniejszy do nauki i debugowania. Minus: dekorator i opakowywany obiekt nie są tożsame (== nie zadziała jak oczekujesz), a kolejność nakładania dekoratorów bywa istotna i łatwo się pomylić.


Decorator w C# — od diagramu do kodu

Scenariusz

Kawiarnia sprzedaje napar z opcjonalnymi dodatkami: mleko, syrop, bita śmietana — każdy zwiększa cenę i dopisuje się do opisu.

Bez wzorca — eksplozja podklas

public class Kawa { public virtual decimal Cena() => 8m; public virtual string Opis() => "Kawa"; }
public class KawaZMlekiem : Kawa { public override decimal Cena() => base.Cena() + 2m; public override string Opis() => base.Opis() + " + mleko"; }
public class KawaZMlekiemISyropem : KawaZMlekiem { public override decimal Cena() => base.Cena() + 3m; public override string Opis() => base.Opis() + " + syrop"; }
// ❌ KawaZSyropem, KawaZBita, KawaZMlekiemIBita... N dodatków = 2^N możliwych podklas

Każda nowa kombinacja dodatków to nowa klasa. Trzy dodatki dają już osiem możliwych kombinacji — osiem potencjalnych klas do napisania i utrzymania.

Z wzorcem — dekoratory składane w dowolnej kombinacji

public interface INapoj
{
    decimal Cena();
    string Opis();
}

public class Kawa : INapoj
{
    public decimal Cena() => 8m;
    public string Opis() => "Kawa";
}

public abstract class DodatekDecorator : INapoj
{
    protected readonly INapoj _napoj;
    protected DodatekDecorator(INapoj napoj) => _napoj = napoj;
    public abstract decimal Cena();
    public abstract string Opis();
}

public class MlekoDecorator : DodatekDecorator
{
    public MlekoDecorator(INapoj napoj) : base(napoj) { }
    public override decimal Cena() => _napoj.Cena() + 2m;
    public override string Opis() => _napoj.Opis() + " + mleko";
}

public class SyropDecorator : DodatekDecorator
{
    public SyropDecorator(INapoj napoj) : base(napoj) { }
    public override decimal Cena() => _napoj.Cena() + 3m;
    public override string Opis() => _napoj.Opis() + " + syrop";
}
INapoj zamowienie = new Kawa();
zamowienie = new MlekoDecorator(zamowienie);
zamowienie = new SyropDecorator(zamowienie);

Console.WriteLine($"{zamowienie.Opis()}: {zamowienie.Cena()} zł");
// "Kawa + mleko + syrop: 13 zł"

Zero nowych klas na kombinację — MlekoDecorator i SyropDecorator można łączyć w dowolnej kolejności i liczbie, w runtime, na podstawie tego, co klient faktycznie zamówił.

Decorator, którego już używasz — System.IO.Stream

Jeśli kiedykolwiek pisałeś coś w stylu:

using Stream plik = File.OpenRead("dane.csv.gz");
using Stream bufor = new BufferedStream(plik);
using Stream rozpakowany = new GZipStream(bufor, CompressionMode.Decompress);
using var reader = new StreamReader(rozpakowany);

— używałeś Decorator Pattern z biblioteki standardowej .NET. FileStreamBufferedStream i GZipStream implementują ten sam interfejs bazowy Stream, a każdy kolejny owija poprzedni, dokładając buforowanie albo dekompresję. Możesz je składać w dowolnej kombinacji — dokładnie tak samo jak MlekoDecorator i SyropDecorator powyżej.

Pułapka: kolejność dekoratorów ma znaczenie

// Kolejność 1: najpierw bufor, potem dekompresja — poprawnie
new GZipStream(new BufferedStream(plik), CompressionMode.Decompress);

// Kolejność 2: najpierw dekompresja, potem próba buforowania nieistniejących
// jeszcze danych — semantycznie co innego, może nie działać jak oczekujesz
new BufferedStream(new GZipStream(plik, CompressionMode.Decompress));

Dekoratory nie są przemienne jak dodawanie liczb — A(B(x)) to nie to samo co B(A(x)), jeśli kolejność operacji ma znaczenie (dekompresja przed czy po buforowaniu, logowanie przed czy po walidacji). Projektując stos dekoratorów, świadomie zdecyduj, w jakiej kolejności mają się wykonywać.

Pułapka: dekorator zmienia tożsamość obiektu

INapoj kawa = new Kawa();
INapoj ozdobiona = new MlekoDecorator(kawa);

Console.WriteLine(ReferenceEquals(kawa, ozdobiona)); // False — to dwa różne obiekty

Po udekorowaniu masz do czynienia z nowym obiektem opakowującym oryginalny, nie z tym samym obiektem z dodaną cechą. Jeśli gdzieś w kodzie porównujesz referencje albo polegasz na konkretnym typie (is Kawa), dekorator to złamie — sprawdzaj przez interfejs INapoj, nie przez konkretny typ.

Podsumowanie

Decorator dokłada zachowanie do obiektu przez owijanie go innym obiektem o tym samym interfejsie, zamiast przez dziedziczenie — dzięki temu można łączyć dowolną liczbę niezależnych cech bez eksplozji podklas. To nie teoria z podręcznika: Stream w .NET jest zbudowany dokładnie w ten sposób. Dwie realne pułapki: kolejność nakładania dekoratorów bywa istotna, a dekorator to inny obiekt niż oryginał — porównuj przez interfejs, nie przez tożsamość referencji.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

Dodaj komentarz

czytanie to początek

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ę →