Przejdź do treści
Programowanie i bazy danych

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.

CZCzarek ZawolskiAktualizacja: 8 min czytania
Diagram klas ilustrujący wzorce projektowe w programowaniu obiektowym

W skrócie

  • 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ść.
Spis treści

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

GrupaCzego dotyczyWzorce GoFPrzykłady w JDK i Springu
Kreacyjne (5)Jak tworzyć obiektySingleton, Metoda wytwórcza, Fabryka abstrakcyjna, Budowniczy, PrototypRuntime.getRuntime(), HttpClient.newBuilder(), DocumentBuilderFactory, Object.clone()
Strukturalne (7)Jak łączyć klasy i obiektyAdapter, Most, Kompozyt, Dekorator, Fasada, Pyłek, PełnomocnikInputStreamReader, 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ącyFilterChain, 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 Connection i Statement, 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ów Integer operatorem == „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());

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:

  1. Zaczynaj od najprostszego kodu. Wzorzec wprowadzaj, gdy pojawia się problem, który rozwiązuje — np. trzeci if wybierający algorytm to dobry moment na strategię.
  2. Sprawdź, czy język lub framework nie robi tego za Ciebie. Lambdy, rekordy, enumy i kontener Springa eliminują sporo klasycznego kodu wzorców.
  3. Nie nadużywaj Singletona. Globalny stan utrudnia testy i ukrywa zależności — lepiej wstrzykiwać zależności przez konstruktor.
  4. Nazywaj rzeczy zgodnie ze wzorcem (...Builder, ...Factory, ...Strategy, ...Adapter). To ułatwia innym zrozumienie kodu bez czytania każdej linii.
  5. 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.

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

CZ

Autor

Czarek Zawolski

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