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

Czym są wzorce projektowe i jak je czytać?

Kąt widzenia: edukacyjny/referencyjny – jak anatomicznie rozumieć wzorzec projektowy
Przykład przewodni: wzorzec Strategy rozkładany na części pierwsze zgodnie z opisem GoF


Problem, który wzorce rozwiązują

Wyobraź sobie, że dołączasz do projektu i widzisz w kodzie klasę PaymentProcessor z metodą Process. Patrzysz na implementację i rozpoznajesz znajomy schemat – kontekst deleguje działanie do wymienialnych obiektów strategii. Mówisz do kolegi: “tu masz Strategy Pattern”. Oboje od razu wiecie o co chodzi – bez czytania 200 linii kodu.

To właśnie pierwsza wartość wzorców projektowych: wspólny język. Nazwy wzorców to skrótowe określenia dobrze znanych rozwiązań. Ale żeby ten język działał, musisz wiedzieć, co naprawdę kryje się pod każdą nazwą – nie tylko “co to jest”, ale jak to czytać i jak rozpoznać w kodzie.

Ten post uczy czytać wzorce. Nie ma tu pełnej listy 23 GoF – jest jeden wzorzec rozkładany na atomy, żebyś wiedział, jak podejść do każdego kolejnego.


GoF — skąd pochodzi podział i dlaczego jest ważny

W 1994 roku czterech autorów (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides – stąd “Gang of Four”) opublikowało Design Patterns: Elements of Reusable Object-Oriented Software. Skatalogowali 23 wzorce podzielone na trzy kategorie:

Kreacyjne (Creational) – kontrolują tworzenie obiektów. Odpowiadają na pytanie: jak tworzyć obiekty, żeby kod był elastyczny i nie wiedział o konkretnych klasach?

Singleton, Factory Method, Abstract Factory, Builder, Prototype

Strukturalne (Structural) — opisują, jak składać klasy i obiekty w większe struktury. Odpowiadają na pytanie: jak łączyć obiekty, żeby zachować elastyczność?

Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Behawioralne (Behavioral) – definiują komunikację i podział odpowiedzialności między obiektami. Odpowiadają na pytanie: kto robi co i jak obiekty ze sobą współpracują?

Chain of Responsibility, Command, Interpreter, Iterator, Mediator,
Memento, Observer, State, Strategy, Template Method, Visitor

Kategoryzacja nie jest celem samym w sobie — pomaga przewidzieć, w jakim kierunku patrzeć gdy masz problem. Problem z tworzeniem obiektów? Patrz w Creational. Problem z komunikacją między klasami? Patrz w Behavioral.


Anatomia wzorca – 8 elementów opisu GoF

Każdy z 23 wzorców w książce GoF jest opisany w tym samym formacie. Jeśli nauczysz się czytać ten format, możesz przeczytać każdy wzorzec — nawet taki, którego nigdy wcześniej nie widziałeś — i zrozumieć, kiedy go użyć.

Rozkładamy Strategy jako przykład:

1. Intent (Zamierzenie)

Definiuje rodzinę algorytmów, hermetyzuje każdy z nich i czyni je wymiennymi. Strategy pozwala na zmianę algorytmu niezależnie od klientów, którzy z niego korzystają.

To jednozdaniowa odpowiedź na pytanie: co ten wzorzec robi? Czytaj Intent jako definicję – musisz ją zapamiętać, bo to właśnie mówisz koledze podczas code review.

Kluczowe słowa w tym Intent: rodzina algorytmów, hermetyzuje, wymienne. Jeśli masz wariant algorytmu, który można podmienić bez zmiany kodu klienckiego – masz kandydata na Strategy.

2. Also Known As (Inne nazwy)

Strategy nie ma innych nazw. Ale np. wzorzec Facade bywa nazywany Wrapper, a Adapter bywa nazywany Wrapper lub Translator. To ważne – w różnych zespołach i książkach możesz spotkać różne nazwy dla tego samego wzorca.

3. Motivation (Motywacja)

Tutaj GoF pokazuje konkretny problem, który doprowadził do wymyślenia wzorca. Dla Strategy:

Wiele algorytmów sortowania. Chcesz móc wybierać algorytm w runtime – bez zmiany klasy, która korzysta z sortowania.

Motywacja to najważniejsza część do nauki – tu rozpoznasz, że masz ten problem w swoim kodzie. Zanim zaczniesz myśleć o wzorcu, zapytaj: czy mój problem wygląda jak problem z Motywacji?

4. Applicability (Kiedy stosować)

Stosuj Strategy gdy:

  • wiele powiązanych klas różni się tylko zachowaniem
  • potrzebujesz różnych wariantów algorytmu
  • algorytm korzysta z danych, których klient nie powinien znać
  • klasa definiuje wiele zachowań i pojawiają się instrukcje warunkowe wybierające między nimi

Ten ostatni punkt to alarm w kodzie: jeśli widzisz if (type == "A") { ... } else if (type == "B") { ... } wewnątrz metody — to Applicability mówi Ci, że możesz mieć kandydata na Strategy.

5. Structure (Struktura)

GoF rysuje diagram klas. Dla Strategy:

┌─────────────┐         ┌──────────────────┐
│   Context   │ ──────> │   <<interface>>  │
│─────────────│         │    IStrategy     │
│ -strategy   │         │──────────────────│
│─────────────│         │ +Execute()       │
│ +SetStrategy│         └──────────────────┘
│ +DoWork()   │                  △
└─────────────┘                  │
                    ┌────────────┴────────────┐
                    │                         │
          ┌─────────────────┐       ┌──────────────────┐
          │  ConcreteStratA │       │  ConcreteStratB  │
          │─────────────────│       │──────────────────│
          │ +Execute()      │       │ +Execute()       │
          └─────────────────┘       └──────────────────┘

Czytaj diagram tak: Context ma referencję do IStrategy. Konkretne implementacje implementują interfejs. Context nie wie, z której konkretnej klasy korzysta — wie tylko o interfejsie.

6. Participants (Uczestnicy)

GoF opisuje każdą klasę z diagramu i jej rolę:

  • Strategy (IStrategy) — wspólny interfejs dla wszystkich algorytmów.
  • ConcreteStrategy — konkretna implementacja algorytmu.
  • Context — konfigurowany przez klienta; przechowuje referencję do strategii i deleguje do niej pracę.

To ważne gdy czytasz kod — szukasz, kto pełni każdą z ról. W realnym projekcie klasy mają inne nazwy niż w GoF.

7. Collaborations (Współpraca)

Context przekazuje wszystkie dane potrzebne do wykonania algorytmu. Klient wybiera strategię i konfiguruje nią Context.

To odpowiedź na pytanie: kto mówi do kogo? Collaborations opisuje przepływ sterowania, nie strukturę klas.

8. Consequences (Konsekwencje)

Plus: eliminuje instrukcje warunkowe. Plus: alternatywa dla dziedziczenia. Minus: klient musi znać różne strategie. Minus: narzut komunikacyjny między Context a Strategy.

To jest najważniejsza część po Applicability. Każdy wzorzec ma wady. GoF je wymienia — nie zakrywa. Minus “klient musi znać różne strategie” oznacza: gdzieś w kodzie musisz zdecydować, którą strategię stworzyć. Jeśli ta decyzja jest skomplikowana — kombinujesz Strategy z Factory.


Strategy w C# – od diagramu do kodu

Teraz przekładamy każdy element opisu na działający kod.

Scenariusz

System płatności obsługuje trzy metody: karta kredytowa, BLIK, przelew bankowy. Każda ma inną logikę walidacji i inne wywołanie do zewnętrznego providera.

Bez wzorca – szybko widać problem:

public class PaymentService
{
    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        if (request.Method == "CreditCard")
        {
            // walidacja karty...
            // wywołanie providera karty...
        }
        else if (request.Method == "Blik")
        {
            // walidacja BLIK...
            // wywołanie BLIK API...
        }
        else if (request.Method == "BankTransfer")
        {
            // walidacja przelewu...
            // wywołanie systemu przelewów...
        }
        // za 6 miesięcy: else if (request.Method == "PayPal") ...
    }
}

Applicability mówi: wiele instrukcji warunkowych wybierających między zachowaniami — to właśnie ten przypadek.

Participant: IStrategy

// Wspólny interfejs — Strategy z diagramu GoF
public interface IPaymentStrategy
{
    bool CanHandle(string paymentMethod);
    Task<PaymentResult> ProcessAsync(PaymentRequest request);
}

Participants: ConcreteStrategy

public class CreditCardPaymentStrategy : IPaymentStrategy
{
    private readonly ICreditCardGateway _gateway;

    public CreditCardPaymentStrategy(ICreditCardGateway gateway)
        => _gateway = gateway;

    public bool CanHandle(string paymentMethod)
        => paymentMethod == "CreditCard";

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Walidacja specyficzna dla karty
        if (request.CardNumber?.Length != 16)
            return PaymentResult.Failure("Niepoprawny numer karty.");

        // Logika specyficzna dla karty
        var charge = await _gateway.ChargeAsync(
            request.CardNumber,
            request.Amount,
            request.Currency);

        return charge.Success
            ? PaymentResult.Success(charge.TransactionId)
            : PaymentResult.Failure(charge.ErrorMessage);
    }
}

public class BlikPaymentStrategy : IPaymentStrategy
{
    private readonly IBlikApi _blikApi;

    public BlikPaymentStrategy(IBlikApi blikApi) => _blikApi = blikApi;

    public bool CanHandle(string paymentMethod) => paymentMethod == "Blik";

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        if (request.BlikCode?.Length != 6)
            return PaymentResult.Failure("Kod BLIK musi mieć 6 cyfr.");

        var result = await _blikApi.AuthorizeAsync(request.BlikCode, request.Amount);

        return result.Authorized
            ? PaymentResult.Success(result.SessionId)
            : PaymentResult.Failure("Autoryzacja BLIK nieudana.");
    }
}

Participant: Context

// Context z diagramu GoF — nie wie, która strategia jest aktywna
public class PaymentService
{
    private readonly IEnumerable<IPaymentStrategy> _strategies;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(
        IEnumerable<IPaymentStrategy> strategies,
        ILogger<PaymentService> logger)
    {
        _strategies = strategies;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Context wybiera strategię — ale nie zna konkretnej klasy
        var strategy = _strategies.FirstOrDefault(s => s.CanHandle(request.Method));

        if (strategy is null)
        {
            _logger.LogWarning("Brak strategii dla metody płatności: {Method}", request.Method);
            return PaymentResult.Failure($"Nieobsługiwana metoda płatności: {request.Method}");
        }

        _logger.LogInformation(
            "Przetwarzam płatność {Amount} {Currency} metodą {Method}",
            request.Amount, request.Currency, request.Method);

        return await strategy.ProcessAsync(request);
    }
}

Collaborations – klient konfiguruje Context przez DI

W ASP.NET Core klient (tu: kontener DI) jest odpowiedzialny za wybór strategii:

// Program.cs — rejestracja wszystkich strategii
builder.Services.AddScoped<IPaymentStrategy, CreditCardPaymentStrategy>();
builder.Services.AddScoped<IPaymentStrategy, BlikPaymentStrategy>();
builder.Services.AddScoped<IPaymentStrategy, BankTransferPaymentStrategy>();
builder.Services.AddScoped<PaymentService>();

// PaymentService dostaje wszystkie — sam wybiera właściwą przez CanHandle()

Dodanie nowej metody płatności = nowa klasa implementująca IPaymentStrategy + jedna linijka rejestracji w DI. PaymentService się nie zmienia.


Jak rozpoznać wzorzec w cudzym kodzie

Gdy czytasz nieznany kod, szukaj elementów z diagramu GoF:

Czego szukaszCo widzisz w kodzie
Strategy (interfejs)interface I*Strategy, interface I*Handler, interface I*Policy
ConcreteStrategyklasy implementujące ten interfejs
Contextklasa z polem typu interfejsu + deleguje do niego wywołanie
Collaborationsmiejsce, gdzie interfejs jest przypisywany (DI, setter, konstruktor)

Wzorce w produkcyjnym kodzie mają inne nazwy niż w GoF. Nie szukaj klasy ConcreteStrategyA – szukaj struktury relacji między klasami.


Konsekwencje – czego nie piszą w tutorialach

GoF wymienia wady Strategy:

Klient musi znać strategie. Gdzieś musisz zadecydować, którą strategię stworzyć. W przykładzie powyżej decyduje DI — ale ktoś musi napisać rejestrację. Przy 15 metodach płatności rejestracja staje się problemem. Rozwiązanie: Factory Method lub reflection-based auto-discovery.

Narzut komunikacyjny. Jeśli algorytm potrzebuje dużo danych z Context, musisz je wszystkie przekazać przez interfejs. Czasem prostszy jest Template Method — algorytm żyje w klasie bazowej i ma dostęp do wszystkich pól.

Proliferacja klas. Każdy wariant algorytmu to osobna klasa. Przy 20 wariantach masz 20 klas. To nie jest problem samo w sobie, ale warto wiedzieć, że tak będzie wyglądać kod.


Szablon do czytania każdego wzorca

Gdy uczysz się nowego wzorca, przejdź przez ten checklist:

1. Intent     — co robi wzorzec? (jedno zdanie)
2. Motivation — jaki problem rozwiązuje? (w jakim kodzie to widzę?)
3. Applicability — kiedy użyć? (jakie sygnały alarmowe w kodzie?)
4. Structure  — kto pełni jaką rolę? (diagram → klasy)
5. Consequences — co zyskuję? co tracę?

Nie musisz zapamiętywać kodu z przykładów — musisz zapamiętać Problem i Strukturę. Kod zawsze możesz sprawdzić. Decyzja “czy tu pasuje Strategy” zależy od zrozumienia Applicability i Consequences.


Podsumowanie

Wzorzec projektowy to nie kod do skopiowania – to opis struktury relacji między klasami, który rozwiązuje konkretny, powtarzalny problem. Strategy pokazuje to dobrze: interfejs + implementacje + kontekst delegujący = eliminacja drabiny if-else, rozszerzalność bez modyfikacji, testowalnośc przez mockowanie.

Ale żeby skorzystać ze wzorca, musisz wiedzieć jak go czytać – Intent mówi co, Applicability mówi kiedy, Consequences mówi za jaką cenę. Reszta to kod, który z tego wynika.


Następny post: Kiedy stosować wzorce projektowe – i kiedy NIE stosować – ten sam temat, zupełnie inna perspektywa: ten sam problem rozwiązany bez wzorca, z nadużyciem wzorców i poprawnie. Kiedy wzorzec pomaga, a kiedy to over-engineering.

👨‍💻
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.

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