~/programowanie cat wzorce-projektowe-java.md
Wzorce projektowe w Javie — 23 wzorce GoF z przykładami w nowoczesnej Javie
Wzorce projektowe w Javie: kreacyjne, strukturalne i behawioralne. Singleton, Builder, Strategy, Decorator i Observer z kodem w Javie 21+ oraz przykładami z JDK.

~ xad tldr wzorce-projektowe-java
- Wzorzec projektowy to sprawdzony, nazwany schemat rozwiązania powtarzalnego problemu w kodzie — nie gotowa biblioteka do skopiowania.
- Klasyczny katalog „Gang of Four” z 1994 r. obejmuje 23 wzorce: 5 kreacyjnych, 7 strukturalnych i 11 behawioralnych.
- Wiele wzorców znajdziesz w samym JDK: Builder (HttpClient.newBuilder), Decorator (strumienie java.io), Strategy (Comparator), Iterator, Proxy.
- Nowoczesna Java upraszcza wzorce: lambdy zastępują klasy strategii i poleceń, enum to najprostszy bezpieczny Singleton, a sealed i switch z dopasowaniem wzorców często zastępują Visitora.
- Stosuj wzorzec, gdy rozwiązuje realny problem w Twoim kodzie — dodawanie ich „na zapas” zwiększa złożoność.
$ tree --spis-tresci
Wzorce projektowe w Javie to sprawdzone, nazwane schematy rozwiązywania powtarzalnych problemów w kodzie obiektowym — od tworzenia obiektów, przez łączenie klas, po komunikację między nimi. Najbardziej znany katalog, opisany w 1994 r. przez tzw. Gang of Four (GoF), obejmuje 23 wzorce podzielone na kreacyjne, strukturalne i behawioralne.
Znajomość wzorców daje dwie rzeczy: gotowe rozwiązania dla typowych problemów oraz wspólny język w zespole. Gdy ktoś mówi „tu wystarczy strategia”, wszyscy wiedzą, o jaki układ klas chodzi. Poniżej znajdziesz przegląd wszystkich grup, przykłady z JDK oraz najważniejsze wzorce zaimplementowane w nowoczesnej Javie (21 i nowszej).
Trzy grupy wzorców projektowych
| Grupa | Czego dotyczy | Wzorce GoF | Przykłady w JDK i Springu |
|---|---|---|---|
| Kreacyjne (5) | Jak tworzyć obiekty | Singleton, Metoda wytwórcza, Fabryka abstrakcyjna, Budowniczy, Prototyp | Runtime.getRuntime(), HttpClient.newBuilder(), DocumentBuilderFactory, Object.clone() |
| Strukturalne (7) | Jak łączyć klasy i obiekty | Adapter, Most, Kompozyt, Dekorator, Fasada, Pyłek, Pełnomocnik | InputStreamReader, BufferedInputStream, SLF4J, Integer.valueOf(), java.lang.reflect.Proxy |
| Behawioralne (11) | Jak obiekty współpracują i dzielą odpowiedzialność | Łańcuch zobowiązań, Polecenie, Interpreter, Iterator, Mediator, Pamiątka, Obserwator, Stan, Strategia, Metoda szablonowa, Odwiedzający | FilterChain, Runnable, Pattern, Iterator, Comparator, AbstractList, FileVisitor |
Nie musisz znać wszystkich 23 wzorców na pamięć. W codziennej pracy z Javą najczęściej spotkasz kilkanaście z nich — omawiam je poniżej.
Wzorce kreacyjne w Javie
Singleton
Singleton gwarantuje istnienie dokładnie jednej instancji klasy i daje do niej globalny dostęp. W Javie najprostszą i bezpieczną (również przy serializacji i refleksji) implementacją jest enum — co zalecał już Joshua Bloch w „Effective Java”:
public enum Konfiguracja {
INSTANCJA;
private final Properties wlasciwosci = new Properties();
public String pobierz(String klucz) {
return wlasciwosci.getProperty(klucz);
}
}
// użycie
String url = Konfiguracja.INSTANCJA.pobierz("db.url");
Jeśli potrzebujesz leniwej inicjalizacji w zwykłej klasie, użyj idiomu „holder” — JVM gwarantuje, że klasa wewnętrzna zostanie załadowana dopiero przy pierwszym wywołaniu i w sposób bezpieczny wątkowo:
public final class Rejestr {
private Rejestr() {}
private static final class Holder {
static final Rejestr INSTANCJA = new Rejestr();
}
public static Rejestr getInstance() {
return Holder.INSTANCJA;
}
}
W aplikacjach Spring Boot rzadko piszesz Singletona ręcznie — beany mają domyślnie zasięg singleton, a kontener sam pilnuje jednej instancji.
Budowniczy (Builder)
Builder pozwala tworzyć obiekty o wielu, często opcjonalnych parametrach bez konstruktorów z dziesięcioma argumentami. W JDK używasz go na co dzień, np. przy HttpClient.newBuilder() czy StringBuilder.
public final class Zamowienie {
private final String klient;
private final List<String> produkty;
private final boolean ekspres;
private Zamowienie(Builder b) {
this.klient = b.klient;
this.produkty = List.copyOf(b.produkty);
this.ekspres = b.ekspres;
}
public static Builder builder(String klient) {
return new Builder(klient);
}
public static final class Builder {
private final String klient;
private final List<String> produkty = new ArrayList<>();
private boolean ekspres;
private Builder(String klient) {
this.klient = Objects.requireNonNull(klient);
}
public Builder produkt(String nazwa) { produkty.add(nazwa); return this; }
public Builder ekspres(boolean wartosc) { this.ekspres = wartosc; return this; }
public Zamowienie build() {
if (produkty.isEmpty()) {
throw new IllegalStateException("Zamówienie musi mieć co najmniej jeden produkt");
}
return new Zamowienie(this);
}
}
}
// użycie
Zamowienie z = Zamowienie.builder("Jan Kowalski")
.produkt("Klawiatura")
.produkt("Mysz")
.ekspres(true)
.build();
Zaletą jest walidacja w jednym miejscu (build()) i niemutowalny obiekt wynikowy. W projektach z Lombokiem ten sam efekt daje adnotacja @Builder.
Metoda wytwórcza i fabryka
Metoda wytwórcza ukrywa przed klientem, jaka konkretna klasa zostanie utworzona. W nowoczesnej Javie dobrze łączy się z interfejsami zapieczętowanymi (sealed), dzięki którym kompilator zna pełną listę implementacji:
public sealed interface Powiadomienie permits Email, Sms {
void wyslij(String tresc);
static Powiadomienie utworz(String kanal) {
return switch (kanal) {
case "email" -> new Email();
case "sms" -> new Sms();
default -> throw new IllegalArgumentException("Nieznany kanał: " + kanal);
};
}
}
final class Email implements Powiadomienie {
public void wyslij(String tresc) { /* wysyłka e-mail */ }
}
final class Sms implements Powiadomienie {
public void wyslij(String tresc) { /* wysyłka SMS */ }
}
Fabryka abstrakcyjna idzie krok dalej: tworzy całe rodziny powiązanych obiektów, np. komplet komponentów interfejsu dla jasnego i ciemnego motywu. Prototyp (kopiowanie istniejących obiektów) jest w Javie rzadziej używany — zamiast clone() zwykle lepiej sprawdza się konstruktor kopiujący.
Wzorce strukturalne w Javie
Adapter i Dekorator na przykładzie java.io
Najlepszą lekcją obu wzorców jest jedna linijka z biblioteki standardowej:
try (var reader = new BufferedReader(
new InputStreamReader(new FileInputStream("dane.txt"), StandardCharsets.UTF_8))) {
reader.lines().forEach(System.out::println);
}
InputStreamReader to adapter — dostosowuje strumień bajtów (InputStream) do interfejsu strumienia znaków (Reader). BufferedReader to dekorator — opakowuje dowolny Reader i dodaje buforowanie oraz metodę lines(), nie zmieniając interfejsu bazowego. Dekoratory można nakładać warstwowo, co daje elastyczność bez mnożenia podklas.
Fasada
Fasada udostępnia prosty interfejs do złożonego podsystemu. Klasycznym przykładem w świecie Javy jest SLF4J (Simple Logging Facade for Java): Twój kod woła logger.info(), a fasada przekazuje to do Logbacka, Log4j 2 czy java.util.logging. Ten sam wzorzec na przykładzie PHP opisujemy w artykule Fasada — wzorzec projektowy.
Pełnomocnik (Proxy)
Proxy ma ten sam interfejs co obiekt docelowy i kontroluje dostęp do niego. To fundament Springa: adnotacje @Transactional, @Cacheable czy @Async działają dzięki temu, że kontener wstrzykuje proxy, które przed i po wywołaniu Twojej metody otwiera transakcję, sprawdza cache lub przenosi wykonanie do innego wątku. Wynika z tego znana pułapka: wywołanie metody @Transactional z innej metody tej samej klasy omija proxy, więc transakcja nie zostanie utworzona.
Kompozyt, Most i Pyłek
- Kompozyt — pozwala traktować pojedynczy obiekt i grupę obiektów jednakowo, np. plik i katalog w drzewie systemu plików albo komponent i kontener w interfejsie graficznym.
- Most — oddziela abstrakcję od implementacji, żeby mogły zmieniać się niezależnie. Przykładem jest JDBC: Twój kod używa interfejsów
ConnectioniStatement, a sterownik konkretnej bazy dostarcza implementację. - Pyłek (Flyweight) — współdzieli niezmienne obiekty, by oszczędzać pamięć.
Integer.valueOf()zwraca obiekty z pamięci podręcznej dla wartości od -128 do 127, dlatego porównywanie obiektówIntegeroperatorem==„czasem działa”, a czasem nie.
Wzorce behawioralne w Javie
Strategia
Strategia pozwala wymieniać algorytm w trakcie działania programu. Od Javy 8 strategia to zwykle po prostu lambda lub referencja do metody — najlepszym przykładem jest Comparator:
List<Produkt> produkty = pobierzProdukty();
Comparator<Produkt> poCenie = Comparator.comparing(Produkt::cena);
Comparator<Produkt> poNazwie = Comparator.comparing(Produkt::nazwa);
produkty.sort(sortujPoCenie ? poCenie : poNazwie.reversed());
Sam algorytm sortowania się nie zmienia — List.sort() dla obiektów używa TimSorta o złożoności O(n log n) — wymieniasz tylko sposób porównywania elementów. Jak na tym tle wypadają inne metody, pokazuje zestawienie złożoności obliczeniowej algorytmów sortowania.
Dla własnej logiki wystarczy interfejs funkcyjny, np. UnaryOperator<BigDecimal> dla różnych sposobów naliczania rabatu. Nie trzeba tworzyć osobnej klasy dla każdego wariantu, o ile logika jest krótka.
Obserwator
Obserwator powiadamia zarejestrowanych odbiorców o zmianie stanu. Klasy java.util.Observer i Observable są przestarzałe od Javy 9, więc w nowym kodzie użyj własnej listy słuchaczy, PropertyChangeListener, mechanizmu zdarzeń Springa (ApplicationEventPublisher) albo bibliotek reaktywnych.
public class Magazyn {
private final List<Consumer<String>> sluchacze = new CopyOnWriteArrayList<>();
public void subskrybuj(Consumer<String> sluchacz) {
sluchacze.add(sluchacz);
}
public void produktDostepny(String nazwa) {
sluchacze.forEach(s -> s.accept(nazwa));
}
}
// użycie
magazyn.subskrybuj(nazwa -> System.out.println("Wyślij e-mail: dostępny " + nazwa));
CopyOnWriteArrayList pozwala bezpiecznie dodawać słuchaczy z innych wątków w trakcie powiadamiania.
Metoda szablonowa i Łańcuch zobowiązań
Metoda szablonowa definiuje szkielet algorytmu w klasie bazowej, a podklasy uzupełniają wybrane kroki. Tak działa np. AbstractList — implementujesz get() i size(), a resztę metod listy dostajesz gratis.
Łańcuch zobowiązań przekazuje żądanie przez kolejne obiekty, z których każdy może je obsłużyć lub przekazać dalej. To zasada działania filtrów serwletów, łańcucha filtrów Spring Security i ogólnie middleware w aplikacjach webowych.
Odwiedzający a sealed i dopasowanie wzorców
Odwiedzający (Visitor) służy do dodawania operacji na hierarchii klas bez modyfikowania tych klas. Jest jednak rozbudowany i trudny do czytania. Od Javy 21 w wielu przypadkach prościej użyć zapieczętowanej hierarchii i switch z dopasowaniem wzorców rekordów — kompilator sam sprawdzi, czy obsłużyłeś wszystkie przypadki:
sealed interface Ksztalt permits Kolo, Prostokat {}
record Kolo(double r) implements Ksztalt {}
record Prostokat(double a, double b) implements Ksztalt {}
static double pole(Ksztalt k) {
return switch (k) {
case Kolo(double r) -> Math.PI * r * r;
case Prostokat(double a, double b) -> a * b;
};
}
Pozostałe wzorce behawioralne to Polecenie (żądanie jako obiekt, w Javie często Runnable lub lambda), Iterator (Iterator i pętla for-each), Stan (zachowanie zależne od stanu obiektu, w Javie często enum z metodami), Mediator, Pamiątka (zapis i odtwarzanie stanu, np. do funkcji „cofnij”) oraz Interpreter (np. java.util.regex.Pattern).
Kiedy stosować wzorce projektowe, a kiedy nie
Wzorce są narzędziem, a nie celem. Kilka praktycznych zasad:
- Zaczynaj od najprostszego kodu. Wzorzec wprowadzaj, gdy pojawia się problem, który rozwiązuje — np. trzeci
ifwybierający algorytm to dobry moment na strategię. - Sprawdź, czy język lub framework nie robi tego za Ciebie. Lambdy, rekordy, enumy i kontener Springa eliminują sporo klasycznego kodu wzorców.
- Nie nadużywaj Singletona. Globalny stan utrudnia testy i ukrywa zależności — lepiej wstrzykiwać zależności przez konstruktor.
- Nazywaj rzeczy zgodnie ze wzorcem (
...Builder,...Factory,...Strategy,...Adapter). To ułatwia innym zrozumienie kodu bez czytania każdej linii. - Pamiętaj o zasadach SOLID — większość wzorców to ich praktyczne zastosowanie, zwłaszcza zasady otwarte-zamknięte i odwrócenia zależności.
Na rozmowach rekrutacyjnych pytania o wzorce zwykle sprawdzają, czy rozumiesz problem, który rozwiązują, a nie czy pamiętasz diagram UML. Jeśli przygotowujesz się do pracy z Javą w zespole, warto też dobrze poznać różnice między Gitem a GitHubem i podstawy projektowania REST API, bo to z nimi wzorce najczęściej się spotykają w codziennym kodzie.
~ man faq
Najczęściej zadawane pytania
Co to są wzorce projektowe w Javie?
To uniwersalne, nazwane sposoby rozwiązywania typowych problemów projektowych w programowaniu obiektowym, np. jak tworzyć złożone obiekty, jak dodać zachowanie bez modyfikowania klasy czy jak powiadamiać inne obiekty o zmianach. Java nie ma ich wbudowanych jako konstrukcji językowych — implementuje się je klasami i interfejsami.
Ile jest wzorców projektowych?
Klasyczna książka „Design Patterns” autorstwa Gammy, Helma, Johnsona i Vlissidesa opisuje 23 wzorce. Poza nimi istnieje wiele innych, np. wzorce architektoniczne (MVC, Repository), wzorce integracyjne czy wzorce współbieżności.
Które wzorce projektowe warto znać na rozmowę rekrutacyjną z Javy?
Najczęściej pojawiają się Singleton (z problemami wielowątkowości), Builder, Factory Method, Strategy, Observer, Decorator, Adapter, Proxy i Template Method, a także pytania o to, gdzie Spring wykorzystuje wzorce, np. proxy w AOP i transakcjach.
Czy Singleton to antywzorzec?
Sam wzorzec nie jest zły, ale bywa nadużywany jako globalna zmienna. Utrudnia testowanie i ukrywa zależności. W aplikacjach ze Springiem czy Jakarta EE lepiej zostawić zarządzanie pojedynczą instancją kontenerowi wstrzykiwania zależności.
Czym różni się Decorator od Proxy?
Strukturalnie są podobne, bo oba opakowują obiekt o tym samym interfejsie. Decorator dodaje nowe zachowanie i można go nakładać warstwowo, a Proxy kontroluje dostęp do obiektu, np. odkłada jego utworzenie, sprawdza uprawnienia lub otacza wywołanie transakcją.
Ten artykuł jest częścią tematu
$ whoami
Założyciel i redaktor XAD.pl. Pisze o sieciach, bezpieczeństwie IT, administracji systemami Windows i Linux oraz o sprzęcie, który sprawia ludziom problemy na co dzień.


