Animowany przewodnik · Wzorce projektowe GoF

23 wzorce GoF przed i po

Każdy wzorzec pokazany tak samo: najpierw kod, który powstaje bez niego, potem kod z nim — i co się dzieje, gdy przychodzi nowe wymaganie. Do tego przykład z prawdziwego projektu i sygnały, po których poznasz, że to właśnie tu. Przewiń — diagramy ruszą same.

bez wzorca → z wzorcem 🧠
przewiń ↓

Jak czytać ten przewodnik

wzorzec to nazwa na rozwiązanie, które już znasz

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

🧠 Zagadka na start

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.

⚠️ Uwaga na wzorcomanię

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.

📌 Bullet points
  • 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 katalogu

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

▶ Symptom → kandydaci kliknij objaw, potem nazwę wzorca
Wybierz symptom.
Mapa jest heurystyką, nie algorytmem: symptom wskazuje kandydatów, a decyzję podejmujesz po przeczytaniu sekcji „Kiedy nie” w rozdziale wzorca.
📌 Bullet points
  • 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.
Część I

Wzorce kreacyjne

Kto i jak tworzy obiekty — żeby kod wołający nie musiał znać konkretnych klas.

Factory Method

podklasa decyduje, co powstaje

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

✗ Bez wzorca
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();
  }
}
✓ Z wzorcem
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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • Wspólny algorytm, w którym różni się tylko to, jaki obiekt jest tworzony w środku.
  • if/switch po typie stoi tuż przed new.
  • 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.

✗ Bez wzorca
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();
  }
}
✓ Z wzorcem
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ą.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 nazwami

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

✗ Bez wzorca
new SearchQuery("buty", null, null,
                100, 300, true, null, 2);
// kategoria? marka? min? max?
// dostępne? sortowanie? strona?
// zamiana 100 z 300 kompiluje się
✓ Z wzorcem
SearchQuery q = SearchQuery.builder()
    .phrase("buty")
    .price(100, 300)
    .onlyAvailable()
    .page(2)
    .build();  // walidacja: min ≤ max

Co 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).

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • Konstruktor z 5+ parametrami, w wywołaniach pełno null i true/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.

✗ Bez wzorca
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) {
    …
  }
}
✓ Z wzorcem
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.

⚠️ Kiedy nie / pułapka

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

🔎 Po czym poznać
  • Kod spoza klasy kopiuje obiekt pole po polu, z instanceof po 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ęcznie

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

✗ Bez wzorca
class OrderRepo {
  private final ConnectionPool pool =
      new ConnectionPool(10);
}
class UserRepo {
  private final ConnectionPool pool =
      new ConnectionPool(10); // druga pula
}
✓ Z wzorcem
// 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • Zasób, którego druga kopia to błąd (pula, cache procesu, rejestr metryk).
  • Kilka miejsc robi new tego samego ciężkiego obiektu.
  • Najpierw sprawdź, czy kontener DI nie rozwiąże tego bez globalnego stanu.
Część II

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 cudzym

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

✗ Bez wzorca
// CheckoutService
if (provider == NEWPAY) {
  try {
    newPay.createCharge(amount.cents(), "PLN");
  } catch (NewPayException e) { … }
} else {
  payCo.charge(amount);
}
// RefundService: to samo jeszcze raz
✓ Z wzorcem
class 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ą.

⚠️ Kiedy nie

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

🔎 Po czym poznać
  • 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 klas

Intencja. 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ę.

✗ Bez wzorca
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 razy
✓ Z wzorcem
interface 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 interfejs

Intencja. 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ść.

✗ Bez wzorca
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 samo
✓ Z wzorcem
interface 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • Dane tworzą drzewo dowolnej głębokości (zestawy, foldery, działy, menu).
  • Ta sama rekurencja z instanceof powtarza się w kilku operacjach.
  • Klient chce traktować „jedną rzecz” i „grupę rzeczy” tak samo.

Decorator

warstwy jak cebula, w dowolnej kombinacji

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

✗ Bez wzorca
class RetryingClient
    extends HttpClient { … }
class RetryingCachingClient
    extends RetryingClient { … }
class LoggingRetryingCachingClient
    extends RetryingCachingClient { … }
// a cache bez retry? kolejna podklasa
✓ Z wzorcem
class 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.

⚠️ Kiedy nie / pułapka

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

🔎 Po czym poznać
  • 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 podsystemu

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

✗ Bez wzorca
// 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);
✓ Z wzorcem
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ć.

⚠️ Kiedy nie

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

🔎 Po czym poznać
  • 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ęści

Intencja. 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ą).

✗ Bez wzorca
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"));
  }
}
✓ Z wzorcem
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ólna

Co to poprawia. Pamięć rośnie z liczbą rodzajów, a nie z liczbą obiektów. Bitmapa ładuje się raz na typ.

⚠️ Kiedy nie

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().

🔎 Po czym poznać
  • 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ęp

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

✗ Bez wzorca
// DocController
if (!user.canRead(doc)) throw new Forbidden();
return repo.load(id);
// ExportJob: własna wersja sprawdzania
// SearchApi: … zapomniane
✓ Z wzorcem
class 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.

🤓 Proxy vs Decorator

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

⚠️ Pułapka

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

🔎 Po czym poznać
  • 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.
Część III

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 ogniwa

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

✗ Bez wzorca
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
}
✓ Z wzorcem
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.

⚠️ Kiedy nie / pułapka

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()).

🔎 Po czym poznać
  • Długa seria if … return na 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, cofaj

Intencja. 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ł.

✗ Bez wzorca
priceButton.onClick(e ->
    priceService.change(selected, price));
moveButton.onClick(e ->
    catalog.move(selected, category));
// cofnąć? ponowić? kto to zrobił?
✓ Z wzorcem
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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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ów

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

✗ Bez wzorca
boolean discount(Cart c) {
  return (c.total() > 200
          && c.has("książki"))
      || c.customer().isVip();
}  // zmiana reguły = deploy
✓ Z wzorcem
interface 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 strukturze

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

✗ Bez wzorca
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 miejscach
✓ Z wzorcem
class 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).

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 koordynatora

Intencja. 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ę.

✗ Bez wzorca
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łe
✓ Z wzorcem
class 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 enkapsulacji

Intencja. 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”.

✗ Bez wzorca
// 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?
✓ Z wzorcem
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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • „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łucha

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

✗ Bez wzorca
void markPaid() {
  status = PAID;
  mailer.sendReceipt(this);
  sms.notifyPaid(this);
  warehouse.startPicking(this);
  // analytics? edytuj Order
}
✓ Z wzorcem
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ń.

⚠️ Kiedy nie / pułapki

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.

🔎 Po czym poznać
  • 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 stanem

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

✗ Bez wzorca
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 switch
✓ Z wzorcem
interface 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 interfejsem

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

✗ Bez wzorca
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;
  };
}
✓ Z wzorcem
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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 podklasach

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

✗ Bez wzorca
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 */ }
✓ Z wzorcem
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.

⚠️ Kiedy nie

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()).

🔎 Po czym poznać
  • 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 klas

Intencja. 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).

✗ Bez wzorca
class Paragraph {
  String toHtml(); byte[] toPdf();
  int words();
}
class Image { /* to samo */ }
class Table { /* to samo */ }
// Markdown = edycja wszystkich trzech
✓ Z wzorcem
interface 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.

⚠️ Kiedy nie

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.

🔎 Po czym poznać
  • 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 instanceof po elementach struktury.

Egzamin + ściąga

sprawdź, co zostało w głowie
Wynik: 0 / 0

Ściąga: wzorzec → sygnał

WzorzecCo robiSygnał w kodzie
Factory Methodpodklasa decyduje, jaki obiekt powstajeif/switch po typie tuż przed new
Abstract Factorytworzy spójną rodzinę obiektówten sam if (provider) w wielu klasach
Builderbuduje złożony obiekt krok po krokukonstruktor z wieloma null/true
Prototypeobiekt sam tworzy swoją kopiękopiowanie pole po polu poza klasą
Singletonjedna instancja, globalny dostępwiele new zasobu, który ma być jeden
Adapterdopasowuje cudzy interfejs do twojegotypy SDK w klasach domenowych
Bridgerozdziela dwie osie zmianklasy AlertSms, ReportSms…
Compositeliść i grupa z tym samym interfejsemrekurencja z instanceof po drzewie
Decoratordokłada zachowanie warstwamiklasy-sklejki LoggingRetrying…
Facadejedno wejście do podsystemuta sama orkiestracja w kilku wejściach
Flyweightwspółdzieli ciężki, niezmienny stantysiące obiektów z tymi samymi danymi
Proxyzastępca kontrolujący dostępto samo sprawdzenie przed każdym użyciem
Chain of Resp.żądanie przez łańcuch ogniwseria if … return na wejściu
Commandoperacja jako obiekt„cofnij”, kolejka, audyt
Interpretermały język jako drzewo obiektówreguły zmieniane bez wdrożenia
Iteratorprzechodzenie bez znajomości strukturyręczne tokeny stron i indeksy
Mediatorkoordynuje współpracę grupykomponenty znają wszystkich sąsiadów
Mementomigawka stanu do przywróceniagettery/settery tylko dla „cofnij”
Observerpowiadamia subskrybentów o zmianieklasa domenowa woła maile, SMS-y…
Statezachowanie zależne od stanuswitch (status) w wielu metodach
Strategywymienny algorytmswitch po „sposobie liczenia”
Template Methodszkielet + kroki w podklasachskopiowany przepływ z jedną różnicą
Visitornowe operacje na stabilnej hierarchiikażda funkcja = metoda w każdej klasie
🎓 Odpowiedź na zagadkę

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.