Jak czytać ten przewodnik
wzorzec to nazwa na rozwiązanie, które już znaszW 1994 roku Erich Gamma, Richard Helm, Ralph Johnson i John Vlissides — „Gang of Four”, w skrócie GoF — wydali książkę Design Patterns: Elements of Reusable Object-Oriented Software. Opisali w niej 23 wzorce w trzech grupach: 5 kreacyjnych (jak tworzyć obiekty), 7 strukturalnych (jak je składać) i 11 behawioralnych (jak rozdzielać odpowiedzialność i komunikację).
Każdy wzorzec w książce ma cztery podstawowe elementy: nazwę, problem, rozwiązanie i konsekwencje. Ten przewodnik skupia się na problemie i konsekwencjach, bo tam jest cała wartość. Diagram klas zapamiętasz w minutę. Trudniej rozpoznać, kiedy go użyć — i kiedy nie.
Każdy rozdział ma ten sam układ: intencja → sytuacja z życia → kod bez wzorca i z wzorcem → animacja → co to poprawia → kiedy nie → 🔎 po czym poznać. Kod jest w Javie (z nowościami do Javy 21), ale wzorce działają tak samo w C#, TypeScripcie czy Kotlinie.
Strategy i State mają identyczny diagram klas: obiekt-kontekst trzyma referencję do interfejsu, a pod nim kilka implementacji. Skoro struktura jest ta sama, to czym te wzorce się różnią? Odpowiedź na końcu.
Wspólny mianownik: co wzorce naprawdę robią
zamieniają „edytuj” na „dodaj”Na początku książki GoF stawia dwie zasady, na których stoi prawie każdy wzorzec:
„Program to an interface, not an implementation.”
„Favor object composition over class inheritance.”
Po polsku: kod wołający ma zależeć od interfejsu, a nie od konkretnej klasy. Zachowanie lepiej składać z obiektów niż dziedziczyć. Efekt jest zwykle podobny: miejsce, które się często zmienia, zostaje odizolowane za interfejsem. Nowe wymaganie oznacza wtedy nową klasę, a nie edycję pięciu starych. Bertrand Meyer już w 1988 roku sformułował zasadę otwarte–zamknięte (moduł otwarty na rozszerzenia, zamknięty na modyfikacje); wersję opartą na interfejsach spopularyzował później Robert C. Martin.
Dlatego każda animacja w tym przewodniku ma licznik. Po kliknięciu 💥 Nowe wymaganie zobaczysz, ile istniejących klas trzeba edytować (✎, na czerwono) i ile nowych dochodzi (+, na zielono) — najpierw bez wzorca, potem z nim.
Wzorzec kosztuje: więcej klas, więcej skoków po kodzie, abstrakcja do zrozumienia. Opłaca się tylko tam, gdzie zmiana faktycznie przychodzi. Dwa przypadki w ifie, które nie zmieniły się od trzech lat, nie potrzebują Strategy. Dobra heurystyka: pierwszy raz napisz wprost, przy drugim podobnym przypadku zanotuj, przy trzecim refaktoryzuj do wzorca.
- Wzorzec = nazwana odpowiedź na powtarzający się problem projektowy, z konsekwencjami.
- Prawie każdy wzorzec izoluje zmienne miejsce za interfejsem albo kompozycją.
- Miara sukcesu: nowe wymaganie dodaje kod, zamiast przepisywać istniejący.
Diagnoza: po czym poznać, że to miejsce na wzorzec
zacznij od bólu, nie od kataloguNikt nie zaczyna dnia od „dziś użyję Mediatora”. Zaczynasz od kodu, który boli: każda zmiana dotyka pięciu plików, switch rośnie co sprint, klasy mnożą się jak króliki. Ten ból ma powtarzalne kształty. Kliknij symptom, który znasz z własnego projektu — zobaczysz kandydatów.
- Najczęstsze sygnały: powtarzany switch po typie, zmiana rozlewająca się po wielu klasach, eksplozja podklas.
- Wzorzec to hipoteza. Sprawdź, czy zmiana, przed którą chroni, naprawdę przychodzi.
Wzorce kreacyjne
Kto i jak tworzy obiekty — żeby kod wołający nie musiał znać konkretnych klas.
Factory Method
podklasa decyduje, co powstajeIntencja. Klasa bazowa definiuje metodę tworzącą obiekt, a podklasy decydują, jaki konkretnie obiekt powstanie. Reszta algorytmu w klasie bazowej pracuje tylko z interfejsem produktu.
Sytuacja. Generator dokumentów sprzedażowych. Proces jest zawsze ten sam: nagłówek, pozycje, stopka, podpis. Różni się format wyjścia: PDF dla klienta, XML dla KSeF (Krajowego Systemu e-Faktur), a za chwilę HTML do podglądu w panelu.
class Document {
byte[] generate(String format) {
Renderer r;
if (format.equals("pdf"))
r = new PdfRenderer();
else if (format.equals("ksef"))
r = new KsefXmlRenderer();
else throw new IllegalArgumentException();
header(r); items(r); footer(r); sign(r);
return r.bytes();
}
}abstract class Document {
final byte[] generate() {
Renderer r = createRenderer();
header(r); items(r); footer(r); sign(r);
return r.bytes();
}
// factory method:
protected abstract Renderer createRenderer();
}
class KsefInvoice extends Document {
protected Renderer createRenderer() {
return new KsefXmlRenderer();
}
}Co to poprawia. Document przestaje znać konkretne renderery. Nowy format to nowa podklasa, a wspólny proces generowania zostaje w jednym miejscu. GoF zauważa, że metody wytwórcze często wołane są właśnie z metod szablonowych (Template Method) — tak jak tutaj.
Jeśli podklasa istnieje tylko po to, żeby nadpisać jedną metodę tworzącą, a nic poza tym jej nie wyróżnia,
prościej przekazać fabrykę (np. Supplier<Renderer>) w konstruktorze. To ta sama idea, tylko
przez kompozycję zamiast dziedziczenia.
Gdzie już to widziałeś: Collection.iterator() w JDK — każda kolekcja
tworzy własną implementację Iterator, a kod wołający zna tylko interfejs.
- Wspólny algorytm, w którym różni się tylko to, jaki obiekt jest tworzony w środku.
if/switchpo typie stoi tuż przednew.- Masz już hierarchię klas (np. typy dokumentów) i każda potrzebuje „swojego” pomocnika.
Abstract Factory
rodzina obiektów, które muszą do siebie pasowaćIntencja. Interfejs do tworzenia całych rodzin powiązanych obiektów bez wskazywania ich konkretnych klas.
Sytuacja. Produkt SaaS wdrażany u klientów na AWS albo na Google Cloud. Aplikacja potrzebuje trzech rzeczy: magazynu plików, kolejki i menedżera sekretów. Na AWS to S3, SQS i Secrets Manager, na GCP — Cloud Storage, Pub/Sub i Secret Manager. Pomieszanie (S3 z Pub/Sub w jednym wdrożeniu) to błąd, który wychodzi dopiero na produkcji.
class UploadService {
Storage storage() {
return cloud == AWS ? new S3Storage()
: new GcsStorage();
}
}
class Notifier { // ta sama decyzja,
Secrets secrets() { // inne miejsce
return cloud == AWS ? new AwsSecrets()
: new GcpSecrets();
}
}interface CloudFactory {
Storage storage();
Queue queue();
Secrets secrets();
}
class AwsFactory implements CloudFactory { … }
class GcpFactory implements CloudFactory { … }
// raz, przy starcie aplikacji:
CloudFactory cloud = cfg.isAws()
? new AwsFactory() : new GcpFactory();
new UploadService(cloud.storage());Co to poprawia. Decyzja „która chmura” zapada raz. Spójność rodziny jest gwarantowana
konstrukcyjnie: fabryka AWS nie zwróci kolejki z GCP. Bonus w testach: InMemoryFactory z fałszywymi
implementacjami podmienia całą infrastrukturę jedną linią.
Dodanie nowego rodzaju produktu (np. cache) wymaga zmiany interfejsu fabryki i wszystkich jej implementacji — GoF wymienia to jako główną wadę wzorca. Jeśli produkty nie muszą do siebie pasować, wystarczą niezależne fabryki albo zwykłe wstrzykiwanie zależności.
- Ten sam
if (provider == …)rozsiany po wielu klasach. - Obiekty mają sens tylko w komplecie (ten sam dostawca, motyw, wersja protokołu).
- Chcesz w testach podmienić całą „platformę” naraz.
Builder
złożony obiekt krok po kroku, z nazwamiIntencja. Oddzielić konstruowanie złożonego obiektu od jego reprezentacji, tak by budować go krok po kroku. W praktyce Javy najczęściej spotkasz odmianę spopularyzowaną przez Joshuę Blocha w Effective Java: builder jako zamiennik konstruktora z wieloma opcjonalnymi parametrami.
Sytuacja. Wyszukiwarka sklepu: fraza, kategoria, marka, cena od–do, tylko dostępne, sortowanie, strona. Większość parametrów jest opcjonalna, a każde wywołanie ustawia inny podzbiór.
new SearchQuery("buty", null, null,
100, 300, true, null, 2);
// kategoria? marka? min? max?
// dostępne? sortowanie? strona?
// zamiana 100 z 300 kompiluje sięSearchQuery q = SearchQuery.builder()
.phrase("buty")
.price(100, 300)
.onlyAvailable()
.page(2)
.build(); // walidacja: min ≤ maxCo to poprawia. Wywołanie czyta się jak zdanie. Nowy parametr z wartością domyślną nie psuje
istniejących wywołań. build() to jedno miejsce na walidację reguł między polami, a wynik może być
niezmienny (immutable).
Trzy obowiązkowe pola to nie powód do buildera — wystarczy konstruktor albo record. Builder dodaje
klasę do utrzymania (chyba że generuje ją np. Lombok @Builder).
Gdzie już to widziałeś: HttpRequest.newBuilder() z
java.net.http (od Javy 11), Stream.builder(), Lombok @Builder.
- Konstruktor z 5+ parametrami, w wywołaniach pełno
nullitrue/false. - Kilka przeciążonych konstruktorów („teleskopowych”), bo każdy klient chce innego podzbioru.
- Reguły między polami (min ≤ max) sprawdzane w różnych miejscach albo wcale.
Prototype
obiekt sam robi swoją kopięIntencja. Tworzyć nowe obiekty przez kopiowanie istniejącego egzemplarza (prototypu) zamiast budowania od zera — bez znajomości jego konkretnej klasy.
Sytuacja. CRM, przycisk „Duplikuj ofertę”. Oferta ma pozycje, rabaty, warunki płatności, a oferty B2B i B2C różnią się polami. Handlowiec kopiuje zeszłoroczną ofertę i zmienia trzy rzeczy.
Offer duplicate(Offer o) {
if (o instanceof B2BOffer b) {
var c = new B2BOffer();
c.setClient(b.getClient());
c.setItems(b.getItems()); // ta sama lista!
c.setDiscounts(b.getDiscounts());
return c;
} else if (o instanceof B2COffer b) {
…
}
}interface Offer { Offer copy(); }
class B2BOffer implements Offer {
public B2BOffer copy() {
var c = new B2BOffer(client);
c.items = items.stream()
.map(Item::copy).toList();
c.discounts = new ArrayList<>(discounts);
return c;
}
}
Offer draft = selected.copy();Co to poprawia. Wiedza „jak mnie skopiować” leży obok pól, których dotyczy. Kod wołający nie
potrzebuje instanceof ani getterów do wszystkiego. Nowe pole i jego kopiowanie zmieniasz w jednej klasie.
Kopia płytka vs głęboka: c.setItems(b.getItems()) sprawia, że oryginał i kopia dzielą jedną listę, więc
edycja kopii zmienia ofertę klienta. W Javie unikaj Object.clone(): Joshua Bloch w Effective
Java zaleca zamiast niego konstruktor kopiujący albo fabrykę kopiującą.
- Kod spoza klasy kopiuje obiekt pole po polu, z
instanceofpo typie. - Użytkownik ma „szablony” albo „duplikuj” w interfejsie.
- Tworzenie od zera jest drogie (konfiguracja, zapytania), a kopie różnią się szczegółami.
Singleton
dokładnie jedna instancja — i dlaczego dziś rzadko piszesz go ręcznieIntencja. Zagwarantować, że klasa ma tylko jedną instancję, i dać do niej globalny punkt dostępu.
Sytuacja. Pula połączeń do bazy danych. Każde repozytorium potrzebuje połączeń, ale pula musi być jedna: kilka pul to kilka razy więcej otwartych połączeń i przekroczony limit bazy.
class OrderRepo {
private final ConnectionPool pool =
new ConnectionPool(10);
}
class UserRepo {
private final ConnectionPool pool =
new ConnectionPool(10); // druga pula
}// wariant eager (GoF tworzy leniwie):
final class ConnectionPool {
private static final ConnectionPool
INSTANCE = new ConnectionPool(10);
private ConnectionPool(int size) { … }
static ConnectionPool getInstance() {
return INSTANCE;
}
}
// dziś częściej: jedna instancja z DI
@Bean DataSource dataSource() { … }Co to poprawia. Jeden zasób, jeden limit, jedno miejsce do strojenia.
Klasyczny Singleton to globalny stan: każda klasa może go zawołać, zależność jest ukryta, a w testach trudno go
podmienić. Dziś „jedną instancję” zwykle zapewnia kontener DI — w Springu beany mają domyślnie zakres
singleton — a klasa dostaje zależność przez konstruktor. Jeśli piszesz Singleton ręcznie,
Effective Java poleca jednoelementowy enum.
Gdzie już to widziałeś: Runtime.getRuntime() w JDK.
- Zasób, którego druga kopia to błąd (pula, cache procesu, rejestr metryk).
- Kilka miejsc robi
newtego samego ciężkiego obiektu. - Najpierw sprawdź, czy kontener DI nie rozwiąże tego bez globalnego stanu.
Wzorce strukturalne
Jak składać klasy i obiekty w większe struktury, nie wiążąc wszystkiego ze wszystkim.
Adapter
przejściówka między twoim interfejsem a cudzymIntencja. Przekształcić interfejs klasy w inny interfejs, którego oczekuje klient, tak by współpracowały klasy o niezgodnych interfejsach.
Sytuacja. Twój sklep płaci przez interfejs PaymentGateway.charge(Money). Dochodzi
nowy operator płatności, którego SDK ma createCharge(long cents, String currency) i własne wyjątki.
// CheckoutService
if (provider == NEWPAY) {
try {
newPay.createCharge(amount.cents(), "PLN");
} catch (NewPayException e) { … }
} else {
payCo.charge(amount);
}
// RefundService: to samo jeszcze razclass NewPayAdapter
implements PaymentGateway {
private final NewPaySdk sdk;
public void charge(Money m) {
try {
sdk.createCharge(m.cents(), m.currency());
} catch (NewPayException e) {
throw new PaymentFailed(e);
}
}
}Co to poprawia. Cudzy interfejs nie wycieka do logiki biznesowej. Przeliczenie jednostek i tłumaczenie błędów są w jednym pliku. Gdy SDK zmieni API albo zmienisz operatora, poprawiasz tylko adapter. W Domain-Driven Design ten sam pomysł w większej skali nazywa się warstwą antykorupcyjną.
Jeśli cudza biblioteka jest jedyną implementacją, jej API jest stabilne i dobrze pasuje do domeny, adapter „na zapas” to tylko dodatkowa warstwa przekazująca wywołania.
Gdzie już to widziałeś: Arrays.asList(array) (tablica widziana jako
List), InputStreamReader (strumień bajtów widziany jako Reader znaków).
- Typy i wyjątki zewnętrznego SDK pojawiają się w klasach domenowych.
- To samo przeliczanie (jednostki, formaty, kody błędów) w kilku miejscach.
- Masz dwóch dostawców tej samej usługi o różnych API.
Bridge
dwie osie zmian zamiast iloczynu klasIntencja. Oddzielić abstrakcję od jej implementacji, tak by obie mogły zmieniać się niezależnie.
Sytuacja. System powiadomień. Rodzaje: alert, raport, przypomnienie (różna treść, priorytet, ponawianie). Kanały: e-mail, SMS, Slack (różna wysyłka). Dziedziczenie po obu osiach naraz daje klasę na każdą kombinację.
class AlertEmail extends Notification {…}
class AlertSms extends Notification {…}
class ReportEmail extends Notification {…}
class ReportSms extends Notification {…}
// 3 rodzaje × 3 kanały = 9 klas,
// kod wysyłki SMS skopiowany 3 razyinterface Channel {
void deliver(String to, String body);
}
abstract class Notification {
protected final Channel channel; // most
Notification(Channel c) { channel = c; }
abstract void send(User u);
}
class Alert extends Notification { … }
new Alert(new SmsChannel());Co to poprawia. Zamiast rodzaje × kanały klas masz rodzaje + kanały. Nowy kanał to jedna klasa, która od razu działa z każdym rodzajem. Kombinację wybierasz w czasie działania programu.
Gdy zmienia się tylko jedna oś (np. kanał, a rodzaj jest jeden), wystarczy zwykła Strategy. Bridge projektuje się zwykle od początku, gdy widać dwie niezależne hierarchie; Adapter dokleja się później do czegoś, co już istnieje.
Gdzie już to widziałeś: JDBC bywa podawany jako przykład: twój kod używa
Connection i Statement, a implementację dostarcza sterownik konkretnej bazy.
- Nazwy klas są złożeniem dwóch słów z dwóch list (
AlertSms,ReportSlack). - Dodanie jednej rzeczy na jednej osi wymaga N nowych klas.
- Kod jednej osi (wysyłka) jest skopiowany w wielu klasach drugiej osi.
Composite
liść i grupa mają ten sam interfejsIntencja. Składać obiekty w struktury drzewiaste i traktować pojedyncze obiekty oraz ich złożenia jednakowo.
Sytuacja. Koszyk w sklepie z zestawami. Zestaw „Startowy pakiet biurowy” zawiera produkty i mniejszy zestaw „Przybory”. Trzeba policzyć cenę (z rabatem zestawu), wagę do wysyłki i dostępność.
BigDecimal price(Object item) {
if (item instanceof Product p)
return p.price();
if (item instanceof Bundle b) {
var sum = ZERO;
for (Object x : b.items())
sum = sum.add(price(x));
return sum.multiply(b.discount());
}
throw new IllegalStateException();
}
// weight(), inStock(): to samointerface Item {
BigDecimal price(); int grams();
}
record Product(BigDecimal price, int grams)
implements Item {}
class Bundle implements Item {
List<Item> items;
public BigDecimal price() {
return items.stream().map(Item::price)
.reduce(ZERO, BigDecimal::add)
.multiply(discount);
}
}Co to poprawia. Rekurencja po drzewie jest w jednym miejscu: w Bundle. Kod koszyka
woła item.price() i nie wie, czy ma przed sobą produkt, czy zestaw z zestawami w środku. Nowy typ pozycji
to nowa klasa implementująca Item.
Gdy struktura nie jest naprawdę rekurencyjna (jeden poziom grupowania), wystarczy zwykła lista. Uważaj też na operacje, które mają sens tylko dla liścia lub tylko dla grupy: wspólny interfejs kusi, by wrzucić do niego wszystko, i wtedy połowa metod rzuca wyjątek.
Gdzie już to widziałeś: AWT: java.awt.Container dziedziczy po
Component i zawiera inne komponenty. Drzewo DOM, system plików, struktura organizacyjna.
- Dane tworzą drzewo dowolnej głębokości (zestawy, foldery, działy, menu).
- Ta sama rekurencja z
instanceofpowtarza się w kilku operacjach. - Klient chce traktować „jedną rzecz” i „grupę rzeczy” tak samo.
Decorator
warstwy jak cebula, w dowolnej kombinacjiIntencja. Dynamicznie dodawać obiektowi odpowiedzialności przez opakowanie go obiektem o tym samym interfejsie. Elastyczna alternatywa dla dziedziczenia.
Sytuacja. Klient HTTP do zewnętrznego API cen. Jeden serwis potrzebuje ponawiania i cache, drugi logowania i metryk, trzeci wszystkiego naraz.
class RetryingClient
extends HttpClient { … }
class RetryingCachingClient
extends RetryingClient { … }
class LoggingRetryingCachingClient
extends RetryingCachingClient { … }
// a cache bez retry? kolejna podklasaclass Retry implements Client {
private final Client next;
Retry(Client next) { this.next = next; }
public Response get(String url) {
for (int i = 1; ; i++) {
try { return next.get(url); }
catch (UncheckedIOException e) {
if (i == 3) throw e;
}
}
}
}
Client c = new Logging(new Retry(
new Cache(new Http())));Co to poprawia. Każdy dodatek to mała klasa z jedną odpowiedzialnością. Kombinacje składasz w konfiguracji zamiast tworzyć podklasę na każdy wariant. Przy n niezależnych dodatkach dziedziczenie potrzebuje do 2n wariantów, dekoratory — n klas.
Kolejność warstw zmienia zachowanie. Retry(Cache(…)) ponawia tylko to, co nie trafiło do cache;
Cache(Retry(…)) zapisuje w cache wynik po ponowieniach. Głęboki stos dekoratorów trudno też debugować:
stack trace ma kilka warstw opakowań.
Gdzie już to widziałeś: new BufferedInputStream(new FileInputStream(f))
w java.io, Collections.unmodifiableList(list).
- Nazwy klas to sklejki przymiotników:
LoggingRetryingCaching…. - Dodatki są niezależne i potrzebne w różnych kombinacjach.
- Chcesz dodać zachowanie do obiektu, którego klasy nie możesz lub nie chcesz zmieniać.
Facade
jedno proste wejście do skomplikowanego podsystemuIntencja. Dać jeden, uproszczony interfejs do zestawu interfejsów podsystemu.
Sytuacja. Złożenie zamówienia: rezerwacja w magazynie, pobranie płatności (z cofnięciem rezerwacji przy błędzie), faktura, zlecenie wysyłki. Zamówienia przychodzą z trzech wejść: strony, aplikacji mobilnej i importu B2B.
// WebController, MobileApi, B2bImport:
stock.reserve(cart);
try {
payments.charge(cart.total());
} catch (PaymentFailed e) {
stock.release(cart); throw e;
}
invoices.issue(cart);
shipping.schedule(cart);class OrderPlacement { // fasada
Order place(Cart cart) {
stock.reserve(cart);
try { payments.charge(cart.total()); }
catch (PaymentFailed e) {
stock.release(cart); throw e;
}
invoices.issue(cart);
return shipping.schedule(cart);
}
}Co to poprawia. Kolejność kroków i kompensacje są w jednym miejscu. Wejścia (web, mobile, B2B) zależą od jednej klasy zamiast od czterech podsystemów. Fasada nie zabrania dostępu do podsystemów — kto potrzebuje szczegółów, nadal może ich użyć.
Fasada, do której trafia każda nowa operacja, rośnie w „klasę-boga”. Trzymaj ją cienką: koordynuje, ale nie zawiera reguł biznesowych podsystemów.
Gdzie już to widziałeś: SLF4J — sama nazwa to Simple Logging Facade for Java. Serwisy aplikacyjne w architekturze warstwowej często pełnią rolę fasady nad domeną.
- Ta sama sekwencja wywołań kilku podsystemów skopiowana w kilku wejściach.
- Klient musi znać kolejność i obsługę błędów cudzych modułów.
- Chcesz, by moduł miał małą, stabilną „powierzchnię” dla reszty systemu.
Flyweight
tysiące obiektów, wspólne ciężkie częściIntencja. Współdzielić obiekty, by wydajnie obsłużyć ogromną liczbę drobnych elementów. Stan dzieli się na wewnętrzny (wspólny, niezmienny — np. bitmapa ikony) i zewnętrzny (unikalny, przekazywany z zewnątrz — np. pozycja).
Sytuacja. Mapa punktów odbioru paczek: dziesiątki tysięcy znaczników, ale tylko kilka rodzajów ikon (paczkomat, punkt partnerski, placówka, punkt z płatnością).
class Marker {
double lat, lon;
BufferedImage icon; // własna kopia bitmapy
Marker(double lat, double lon, String type)
throws IOException {
this.lat = lat; this.lon = lon;
icon = ImageIO.read(
new File("icons/" + type + ".png"));
}
}class IconFactory {
private final Map<String, Icon> cache =
new HashMap<>();
Icon get(String type) {
return cache.computeIfAbsent(
type, Icon::load);
}
}
record Marker(double lat, double lon,
Icon icon) {} // ikona wspólnaCo to poprawia. Pamięć rośnie z liczbą rodzajów, a nie z liczbą obiektów. Bitmapa ładuje się raz na typ.
Przy setkach obiektów zysk jest pomijalny, a kod się komplikuje. Współdzielony stan musi być niezmienny: jeśli ktoś pokoloruje „swoją” ikonę, zmieni ją wszystkim. Zanim zaczniesz, zmierz pamięć profilerem.
Gdzie już to widziałeś: Integer.valueOf(int) zwraca obiekty z cache dla
wartości od −128 do 127, dlatego == na małych Integer „działa”, a na dużych nie.
String.intern().
- Bardzo dużo obiektów, a profiler pokazuje, że pamięć zjadają powtarzające się dane.
- Większość stanu obiektów jest identyczna, różnią się kilkoma polami (pozycja, liczba).
- Wspólna część może być niezmienna.
Proxy
zastępca, który kontroluje dostępIntencja. Dostarczyć zastępcę obiektu, który kontroluje dostęp do niego. Odmiany z GoF: remote (obiekt jest w innym procesie), virtual (leniwe tworzenie drogiego obiektu), protection (kontrola uprawnień).
Sytuacja. System dokumentów wielu klientów (multi-tenant). Dokumenty czytają kontroler, eksport i wyszukiwarka. Każde z tych miejsc musi sprawdzić uprawnienia.
// DocController
if (!user.canRead(doc)) throw new Forbidden();
return repo.load(id);
// ExportJob: własna wersja sprawdzania
// SearchApi: … zapomnianeclass SecuredDocRepo
implements DocumentRepo {
private final DocumentRepo real;
public Document load(DocId id) {
Document d = real.load(id);
if (!access.canRead(user(), d))
throw new Forbidden(id);
audit.log(user(), id);
return d;
}
}Co to poprawia. Klient nie ma referencji do prawdziwego repozytorium, więc nie może ominąć kontroli. Reguła dostępu i audyt żyją w jednym miejscu.
Struktura jest prawie identyczna: obiekt o tym samym interfejsie trzyma referencję do innego. Różni je intencja. Decorator dodaje zachowanie i zwykle składasz go dowolnie. Proxy kontroluje dostęp do obiektu i często sam zarządza jego cyklem życia (np. tworzy go leniwie).
Spring realizuje @Transactional przez proxy. Gdy metoda wywoła inną metodę tego samego obiektu
(this.save()), wywołanie omija proxy, więc transakcja z adnotacji na wywoływanej metodzie nie zadziała.
Leniwe proxy Hibernate potrafią z kolei rzucić LazyInitializationException po zamknięciu sesji.
Gdzie już to widziałeś: java.lang.reflect.Proxy, proxy Spring AOP,
leniwe encje Hibernate, klienci RPC (gRPC stub to remote proxy).
- To samo sprawdzenie (uprawnienia, limit, cache) przed każdym użyciem obiektu.
- Obiekt jest drogi do utworzenia, a często nie jest w ogóle użyty.
- Obiekt jest zdalny, a chcesz go wołać jak lokalny.
Wzorce behawioralne
Kto za co odpowiada i jak obiekty rozmawiają — żeby zmiana reguły nie wymagała zmiany rozmówców.
Chain of Responsibility
żądanie wędruje przez ogniwaIntencja. Uniknąć wiązania nadawcy żądania z odbiorcą: przekazywać żądanie wzdłuż łańcucha obiektów, z których każdy może je obsłużyć, przerwać albo przekazać dalej.
Sytuacja. Endpoint API zamówień. Przed logiką: uwierzytelnienie, limit zapytań, walidacja JSON-a. Każdy krok może zakończyć żądanie odpowiedzią błędu.
Response handle(Request r) {
if (!tokens.valid(r.auth()))
return status(401);
if (limiter.exceeded(r.clientId()))
return status(429);
if (!schema.valid(r.body()))
return status(400);
// dopiero tu logika zamówienia
}class RateLimit extends Handler {
Response handle(Request r) {
if (limiter.exceeded(r.clientId()))
return status(429); // przerwij
return next.handle(r); // albo dalej
}
}
auth.then(rateLimit)
.then(validation)
.then(orderEndpoint);Co to poprawia. Każdy krok to osobna, testowalna klasa. Łańcuch składasz konfiguracją, a nowy krok (np. blokada IP) wpinasz bez dotykania endpointów.
Kolejność ogniw ma znaczenie (limit przed uwierzytelnieniem liczy też zapytania anonimowe). GoF wprost ostrzega: nie ma gwarancji, że ktoś obsłuży żądanie — źle złożony łańcuch może je po cichu zgubić. Dodaj ogniwo końcowe albo test składania łańcucha.
Gdzie już to widziałeś: Filter i FilterChain.doFilter() w
Servlet API, łańcuch filtrów Spring Security, middleware w Express (next()).
- Długa seria
if … returnna początku metody, kopiowana między endpointami. - Kroki przetwarzania mają się zmieniać lub być różne dla różnych tras.
- Nie wiadomo z góry, kto obsłuży żądanie (eskalacja: L1 → L2 → L3 wsparcia).
Command
operacja jako obiekt: kolejkuj, loguj, cofajIntencja. Zamknąć żądanie w obiekcie. Dzięki temu można je parametryzować, kolejkować, logować i cofać.
Sytuacja. Panel administracyjny sklepu z operacjami masowymi: zmiana ceny 300 produktów, przeniesienie do kategorii. Biznes chce „Cofnij” i dziennik, kto co zmienił.
priceButton.onClick(e ->
priceService.change(selected, price));
moveButton.onClick(e ->
catalog.move(selected, category));
// cofnąć? ponowić? kto to zrobił?class ChangePrice implements Command {
private final Map<Sku, Money> old =
new HashMap<>();
public void execute() {
for (Sku s : skus) {
old.put(s, prices.get(s));
prices.set(s, price);
}
}
public void undo() { old.forEach(prices::set); }
}
history.run(new ChangePrice(selected, price));Co to poprawia. Operacja przestaje być ulotnym wywołaniem metody. Obiekt polecenia można odłożyć na stos (cofanie), zapisać (audyt), wysłać do kolejki (wykonanie w tle) albo ponowić po błędzie.
Jeśli nie potrzebujesz cofania, kolejkowania ani historii, klasa na każdą akcję to zbędny narzut — wystarczy
lambda. Cofanie bywa też trudne: wysłanego maila nie da się „odwysłać”, więc undo() musi być operacją
kompensującą, a nie magicznym przewinięciem czasu.
Gdzie już to widziałeś: Runnable i Callable przekazywane do
ExecutorService (polecenie bez cofania), javax.swing.Action, Ctrl+Z w edytorach.
- Wymaganie „cofnij/ponów”, historia operacji, audyt.
- Operację trzeba wykonać później, w innym wątku albo procesie.
- Te same akcje są wyzwalane z wielu miejsc (przycisk, skrót, API).
Interpreter
mały język jako drzewo obiektówIntencja. Dla prostego języka zdefiniować reprezentację gramatyki (klasa na regułę) i interpreter, który wykonuje zdania tego języka, zwykle jako drzewo składniowe (AST).
Sytuacja. Dział marketingu co tydzień zmienia reguły rabatów: „koszyk powyżej 200 zł i kategoria książki, albo klient VIP”. Każda zmiana to dziś zadanie dla programisty i wdrożenie.
boolean discount(Cart c) {
return (c.total() > 200
&& c.has("książki"))
|| c.customer().isVip();
} // zmiana reguły = deployinterface Rule { boolean eval(Cart c); }
record Gt(int min) implements Rule {
public boolean eval(Cart c) {
return c.total() > min;
}
}
record And(Rule l, Rule r) implements Rule {
public boolean eval(Cart c) {
return l.eval(c) && r.eval(c);
}
}
// tekst z konfiguracji → drzewo:
Rule rule = parser.parse(text);Co to poprawia. Reguły stają się danymi, które można zapisać w bazie i zmieniać bez wdrożenia. Każdy węzeł jest prosty i testowalny. Nowy rodzaj warunku to jedna klasa.
GoF zaznacza, że wzorzec sprawdza się przy prostych gramatykach. Dla złożonych języków lepszy jest generator parserów (np. ANTLR) albo gotowy język wyrażeń (SpEL, CEL). Pamiętaj też, że reguły pisane przez biznes nadal wymagają testów i wersjonowania.
Gdzie już to widziałeś: Spring Expression Language (SpEL), filtry wyszukiwania typu
status:open AND label:bug, wyrażenia reguł w systemach alertów.
- Biznes chce zmieniać warunki bez wdrożenia.
- Warunki składają się z kilku powtarzalnych klocków (porównanie, AND, OR, NOT).
- Te same reguły trzeba też wyświetlić, zwalidować albo przetłumaczyć (np. na SQL).
Iterator
przechodzenie bez wiedzy o strukturzeIntencja. Sekwencyjny dostęp do elementów kolekcji bez ujawniania jej wewnętrznej reprezentacji.
Sytuacja. Zewnętrzne API faktur zwraca dane stronami po 100, z tokenem następnej strony. Raport miesięczny, synchronizacja i eksport przechodzą po wszystkich fakturach.
String token = null;
do {
Page p = api.list(token);
for (Invoice i : p.items()) process(i);
token = p.nextPageToken();
} while (token != null);
// ta pętla w pięciu miejscachclass InvoiceIterator
implements Iterator<Invoice> {
public boolean hasNext() {
while (!page.hasNext() && !last) {
Page p = api.list(token);
page = p.items().iterator();
token = p.nextPageToken();
last = token == null;
}
return page.hasNext();
}
}
for (Invoice i : api.invoices()) process(i);Co to poprawia. Wiedza o stronicowaniu jest w jednym miejscu. Kod biznesowy pisze zwykłą pętlę
for, a następna strona jest pobierana dopiero wtedy, gdy jest potrzebna (leniwie).
Dla zwykłych kolekcji iterator już masz — nie pisz własnego. Uważaj na modyfikację kolekcji w trakcie przechodzenia:
iteratory kolekcji z java.util typu fail-fast rzucają wtedy ConcurrentModificationException.
Gdzie już to widziałeś: Iterator i Iterable (każda pętla
for-each), paginatory w AWS SDK for Java 2.x, generatory w Pythonie i JavaScripcie.
- Klient ręcznie zarządza indeksami, kursorami lub tokenami stron.
- Ta sama pętla przechodzenia jest skopiowana w wielu miejscach.
- Chcesz ukryć, czy dane są w pamięci, w bazie, czy w zdalnym API.
Mediator
wszyscy rozmawiają przez jednego koordynatoraIntencja. Zamknąć w osobnym obiekcie sposób, w jaki współpracuje grupa obiektów. Obiekty nie odwołują się do siebie bezpośrednio, więc są luźno powiązane.
Sytuacja. Formularz rezerwacji w restauracji. Data zmienia dostępne godziny. Liczba gości zmienia stoliki i godziny (duże grupy tylko wieczorem). Kod promocyjny zmienia cenę.
date.onChange(d -> {
slots.reload(d);
tables.reload(d, guests.count());
});
guests.onChange(n -> {
tables.reload(date.value(), n);
slots.filterForGroup(n);
});
// każdy komponent zna pozostałeclass BookingForm { // mediator
void changed(Component c) {
if (c == date || c == guests) {
slots.reload(date.value(),
guests.count());
tables.reload(date.value(),
guests.count());
}
if (c == promo) price.recalculate();
}
}
date.onChange(v -> form.changed(date));Co to poprawia. Sieć zależności „każdy z każdym” (do N·(N−1) połączeń) zamienia się w gwiazdę (N połączeń). Komponenty są wielokrotnego użytku, bo nie znają sąsiadów. Reguły współpracy czytasz w jednej klasie.
GoF ostrzega, że mediator może stać się monolitem trudnym w utrzymaniu: cała złożoność przenosi się do niego. Przy dwóch–trzech komponentach bezpośrednie wywołania są czytelniejsze.
Gdzie już to widziałeś: kontroler formularza w UI, wieża kontroli lotów (samoloty nie rozmawiają ze sobą, tylko z wieżą), czat grupowy.
- Komponenty mają referencje do wielu sąsiadów i nawzajem się aktualizują.
- Nie da się użyć komponentu w innym formularzu, bo „zna” konkretnych sąsiadów.
- Reguły interakcji są rozsiane po handlerach zdarzeń.
Memento
migawka stanu bez łamania enkapsulacjiIntencja. Bez naruszania enkapsulacji uchwycić i zapisać wewnętrzny stan obiektu, tak by później można było go przywrócić. Role: originator (obiekt ze stanem), memento (migawka), caretaker (przechowuje migawki, nie zagląda do środka).
Sytuacja. Konfigurator komputera w sklepie: procesor, RAM, karta graficzna, zasilacz. Klient zmienia kartę, zasilacz dopasowuje się automatycznie, a klient chce „Cofnij”.
// ConfiguratorUI
var cpu = config.getCpu();
var ram = config.getRam();
var gpu = config.getGpu();
config.setGpu(choice);
// Cofnij:
config.setCpu(cpu);
config.setRam(ram);
config.setGpu(gpu); // a zasilacz?class PcConfig {
Snapshot save() {
return new Snapshot(cpu, ram, gpu, psu);
}
void restore(Snapshot s) {
cpu = s.cpu; ram = s.ram;
gpu = s.gpu; psu = s.psu;
}
static final class Snapshot { // pola private
private final Cpu cpu; …
}
}
undo.push(config.save());Co to poprawia. Tylko PcConfig wie, z czego składa się jego stan. UI przechowuje
nieprzezroczyste migawki. Nowe pole nie wymaga zmian w UI. W Javie klasa zewnętrzna widzi prywatne pola swojej
klasy zagnieżdżonej, więc Snapshot jest czytelny tylko dla PcConfig.
Duży stan i częste migawki to dużo pamięci. Wtedy lepiej zapisywać zmiany (Command z undo()) zamiast
całych migawek. Migawka zawierająca referencje do obiektów zmiennych nie jest prawdziwą migawką — kopiuj albo
używaj obiektów niezmiennych.
Gdzie już to widziałeś: cofanie w edytorach, zapis stanu gry, migawki (snapshoty) agregatów w event sourcingu.
- „Cofnij” albo „przywróć wersję” w wymaganiach.
- Kod spoza klasy czyta i zapisuje jej pola tylko po to, żeby zrobić kopię zapasową.
- Klasa dostała gettery i settery wyłącznie na potrzeby zapisu stanu.
Observer
ogłoś zmianę, nie wiedząc, kto słuchaIntencja. Zależność jeden-do-wielu: gdy obiekt zmienia stan, wszyscy zarejestrowani obserwatorzy są automatycznie powiadamiani.
Sytuacja. Zamówienie zostaje opłacone. Mail z potwierdzeniem, SMS, start kompletacji w magazynie, a za miesiąc dział analityki chce też dostawać to zdarzenie.
void markPaid() {
status = PAID;
mailer.sendReceipt(this);
sms.notifyPaid(this);
warehouse.startPicking(this);
// analytics? edytuj Order
}void markPaid() {
status = PAID;
bus.publish(new OrderPaid(id));
}
// przy składaniu aplikacji:
bus.subscribe(OrderPaid.class,
mailer::onPaid);
bus.subscribe(OrderPaid.class,
warehouse::onPaid);Co to poprawia. Klasa domenowa nie zależy od maili, SMS-ów ani magazynu. Nowy odbiorca to nowa klasa plus jedna linia rejestracji. Odbiorców można włączać i wyłączać bez zmiany źródła zdarzeń.
Przepływ staje się niejawny: z kodu markPaid() nie widać, co się wydarzy. Kolejność powiadomień zwykle
nie jest gwarantowana. Wyjątek w jednym obserwatorze może zatrzymać pozostałych. Zapomniane wyrejestrowanie trzyma
obiekt w pamięci (tzw. lapsed listener). Jeśli kroki muszą wykonać się razem i w kolejności, wywołaj je jawnie.
Gdzie już to widziałeś: java.util.Observer (oznaczony jako
@Deprecated od Javy 9), PropertyChangeListener, @EventListener w Springu,
addEventListener w przeglądarce, Reactive Streams.
- Klasa domenowa woła wprost usługi, które nie należą do jej odpowiedzialności.
- Lista „po X zrób jeszcze Y” rośnie z każdym sprintem.
- Kilka widoków musi pokazywać ten sam, zmieniający się stan.
State
obiekt zmienia zachowanie razem ze stanemIntencja. Pozwolić obiektowi zmieniać zachowanie, gdy zmienia się jego wewnętrzny stan — z zewnątrz wygląda to, jakby obiekt zmienił klasę.
Sytuacja. Cykl życia zamówienia: nowe → opłacone → wysłane, w każdej chwili do pewnego momentu
anulowane. Metody pay(), ship(), cancel() robią w każdym stanie co innego albo
są zabronione.
void ship() {
switch (status) {
case NEW -> throw new
IllegalStateException("nieopłacone");
case PAID -> {
carrier.book(this); status = SHIPPED;
}
case SHIPPED, CANCELLED ->
throw new IllegalStateException();
}
} // pay(), cancel(): ten sam switchinterface OrderState {
default OrderState ship(Order o) {
throw new IllegalStateException();
}
}
class PaidState implements OrderState {
public OrderState ship(Order o) {
o.carrier().book(o);
return new ShippedState();
}
}
// Order:
void ship() { state = state.ship(this); }Co to poprawia. Reguły jednego stanu są w jednej klasie, a nie rozsmarowane po wszystkich metodach. Przejścia są jawne. Nowy stan to nowa klasa plus zmiana w stanach, z których się do niego przechodzi.
Dwa–trzy stany i jedna metoda, która od nich zależy — enum ze switchem jest czytelniejszy. Przy wielu stanach i przejściach z warunkami rozważ bibliotekę maszyny stanów (np. Spring Statemachine) albo tabelę przejść.
Gdzie już to widziałeś: GoF ilustruje ten wzorzec połączeniem TCP
(TCPConnection ze stanami Established, Listen, Closed). Workflow dokumentów, odtwarzacz multimediów.
- Ten sam
switch (status)w wielu metodach jednej klasy. - Błędy typu „dało się wysłać nieopłacone zamówienie”.
- Diagram stanów z wymagań nie ma odpowiednika w kodzie.
Strategy
wymienny algorytm za jednym interfejsemIntencja. Zdefiniować rodzinę algorytmów, zamknąć każdy w osobnej klasie i uczynić je wymiennymi. Algorytm zmienia się niezależnie od klientów, którzy go używają.
Sytuacja. Koszt dostawy w sklepie. Paczkomat: cena zależy od gabarytu. Kurier: od wagi i strefy. Odbiór osobisty: zero. Do tego promocje, które dotyczą tylko niektórych przewoźników.
Money shipping(Cart c, Carrier k) {
return switch (k) {
case LOCKER -> lockerSize(c).price();
case COURIER -> zoneRate(c.address())
.times(c.weightKg());
case PICKUP -> Money.ZERO;
};
}interface ShippingStrategy {
Money cost(Cart c);
}
class Courier implements ShippingStrategy {
public Money cost(Cart c) {
return zoneRate(c.address())
.times(c.weightKg());
}
}
// Checkout:
c.subtotal().plus(shipping.cost(c));Co to poprawia. Każdy algorytm testujesz osobno. Nowy przewoźnik nie dotyka istniejących. Strategię wybiera klient (tu: kupujący w koszyku) albo konfiguracja.
Gdy algorytmy są dwa i się nie zmieniają, switch jest prostszy. W nowoczesnej Javie strategia często jest po prostu
lambdą (Function<Cart, Money>) — klasa jest potrzebna dopiero, gdy algorytm ma stan albo kilka metod.
Gdzie już to widziałeś: Comparator przekazany do
List.sort(), RetryPolicy i BackoffPolicy w bibliotekach do ponawiania.
- Switch po „sposobie liczenia”, który rośnie z każdym nowym wariantem.
- Warianty trzeba wybierać w czasie działania (konfiguracja, wybór użytkownika, test A/B).
- Chcesz testować każdy wariant w izolacji.
Template Method
szkielet w klasie bazowej, kroki w podklasachIntencja. Zdefiniować szkielet algorytmu w metodzie klasy bazowej, a niektóre kroki zostawić podklasom. Podklasy zmieniają kroki, ale nie strukturę algorytmu.
Sytuacja. Import wyciągów bankowych do księgowości. Bank A wysyła CSV, bank B XML. Poza parsowaniem wszystko jest wspólne: odczyt, walidacja (np. duplikaty transakcji), zapis, raport.
class BankACsvImport {
void run(Path f) {
var rows = parseCsv(read(f));
validate(rows); // skopiowane
ledger.save(rows); // skopiowane
report(rows); // skopiowane
}
}
class BankBXmlImport { /* to samo */ }abstract class BankImport {
public final void run(Path f) {
var rows = parse(read(f));
validate(rows);
ledger.save(rows);
report(rows);
}
protected abstract List<Txn> parse(
byte[] raw);
}
class CsvImport extends BankImport { … }Co to poprawia. Wspólny przepływ i poprawki błędów są w jednym miejscu. final na
metodzie szablonowej pilnuje, żeby podklasa nie zmieniła kolejności kroków.
Wzorzec opiera się na dziedziczeniu, więc dziedziczy jego wady: jedna klasa bazowa, ścisłe powiązanie, trudność w łączeniu wariantów. Gdy kroków do podmiany jest kilka i niezależnych, przekaż je jako strategie (kompozycja).
Gdzie już to widziałeś: AbstractList (dla listy tylko do odczytu
wystarczy zaimplementować get(int) i size()), HttpServlet.service() wywołujący
doGet() lub doPost(), setUp()/tearDown() w JUnit 3
(TestCase.runBare()).
- Dwie lub więcej klas z prawie identyczną metodą, różniącą się jednym–dwoma krokami.
- Poprawka błędu trzeba nanosić w kilku kopiach.
- Kolejność kroków jest regułą, której nikt nie powinien zmieniać.
Visitor
nowe operacje bez dotykania klasIntencja. Reprezentować operację wykonywaną na elementach struktury obiektów jako osobną klasę.
Nowe operacje dodajesz bez zmiany klas elementów. Mechanizm: podwójna dyspozycja (double dispatch) —
element.accept(v) woła v.visit(this), więc wybór metody zależy od typu elementu i typu
wizytatora.
Sytuacja. Edytor dokumentów z elementami: akapit, obraz, tabela. Operacje: eksport do HTML, do PDF, liczenie słów, a za chwilę eksport do Markdown i sprawdzanie dostępności (alt-teksty).
class Paragraph {
String toHtml(); byte[] toPdf();
int words();
}
class Image { /* to samo */ }
class Table { /* to samo */ }
// Markdown = edycja wszystkich trzechinterface Visitor<R> {
R visit(Paragraph p);
R visit(Image i);
R visit(Table t);
}
class Paragraph implements Element {
public <R> R accept(Visitor<R> v) {
return v.visit(this);
}
}
class MarkdownVisitor
implements Visitor<String> { … }Co to poprawia. Cała logika jednej operacji (np. HTML) jest w jednej klasie, a klasy elementów nie znają formatów eksportu. Nowa operacja to nowa klasa.
Visitor zamienia jedną trudność na drugą: nowe operacje są tanie, ale nowy typ elementu wymaga
zmiany każdego wizytatora. Używaj go, gdy hierarchia elementów jest stabilna. W Javie 21 alternatywą jest
sealed interface + switch z dopasowaniem wzorców: kompilator sprawdza, czy obsłużyłeś
wszystkie typy, a klasy elementów nie potrzebują accept().
Gdzie już to widziałeś: FileVisitor w
Files.walkFileTree(), ElementVisitor w API procesorów adnotacji, kompilatory i lintery
przechodzące po AST.
- Każda nowa funkcja (eksport, walidacja, statystyka) to nowa metoda w każdej klasie hierarchii.
- Hierarchia typów prawie się nie zmienia, a operacji ciągle przybywa.
- Kod pełen
instanceofpo elementach struktury.
Egzamin + ściąga
sprawdź, co zostało w głowieŚciąga: wzorzec → sygnał
| Wzorzec | Co robi | Sygnał w kodzie |
|---|---|---|
| Factory Method | podklasa decyduje, jaki obiekt powstaje | if/switch po typie tuż przed new |
| Abstract Factory | tworzy spójną rodzinę obiektów | ten sam if (provider) w wielu klasach |
| Builder | buduje złożony obiekt krok po kroku | konstruktor z wieloma null/true |
| Prototype | obiekt sam tworzy swoją kopię | kopiowanie pole po polu poza klasą |
| Singleton | jedna instancja, globalny dostęp | wiele new zasobu, który ma być jeden |
| Adapter | dopasowuje cudzy interfejs do twojego | typy SDK w klasach domenowych |
| Bridge | rozdziela dwie osie zmian | klasy AlertSms, ReportSms… |
| Composite | liść i grupa z tym samym interfejsem | rekurencja z instanceof po drzewie |
| Decorator | dokłada zachowanie warstwami | klasy-sklejki LoggingRetrying… |
| Facade | jedno wejście do podsystemu | ta sama orkiestracja w kilku wejściach |
| Flyweight | współdzieli ciężki, niezmienny stan | tysiące obiektów z tymi samymi danymi |
| Proxy | zastępca kontrolujący dostęp | to samo sprawdzenie przed każdym użyciem |
| Chain of Resp. | żądanie przez łańcuch ogniw | seria if … return na wejściu |
| Command | operacja jako obiekt | „cofnij”, kolejka, audyt |
| Interpreter | mały język jako drzewo obiektów | reguły zmieniane bez wdrożenia |
| Iterator | przechodzenie bez znajomości struktury | ręczne tokeny stron i indeksy |
| Mediator | koordynuje współpracę grupy | komponenty znają wszystkich sąsiadów |
| Memento | migawka stanu do przywrócenia | gettery/settery tylko dla „cofnij” |
| Observer | powiadamia subskrybentów o zmianie | klasa domenowa woła maile, SMS-y… |
| State | zachowanie zależne od stanu | switch (status) w wielu metodach |
| Strategy | wymienny algorytm | switch po „sposobie liczenia” |
| Template Method | szkielet + kroki w podklasach | skopiowany przepływ z jedną różnicą |
| Visitor | nowe operacje na stabilnej hierarchii | każda funkcja = metoda w każdej klasie |
Strategy i State różnią się intencją i tym, kto decyduje o zmianie. W Strategy implementację wybiera klient
(kupujący wybiera kuriera), a strategie zwykle nic o sobie nie wiedzą. W State stany znają przejścia i same
podmieniają stan kontekstu (PaidState.ship() zwraca ShippedState). Ta sama lekcja wraca
przy Decorator vs Proxy i Adapter vs Bridge: wzorzec to nie kształt diagramu, tylko problem, który rozwiązuje.
Następny krok: weź moduł ze swojego projektu, który najczęściej zmieniasz, i przejrzyj go z listą symptomów z rozdziału 2.