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 szukasz | Co widzisz w kodzie |
|---|---|
| Strategy (interfejs) | interface I*Strategy, interface I*Handler, interface I*Policy |
| ConcreteStrategy | klasy implementujące ten interfejs |
| Context | klasa z polem typu interfejsu + deleguje do niego wywołanie |
| Collaborations | miejsce, 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.
🚀 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ę →
7 comments