Паттерны проектирования в Java: виды и примеры кода

Паттерны проектирования в Java: виды и примеры кода Полезное

Паттерн проектирования — это описание типового решения повторяющейся задачи проектирования: какие роли есть у классов и объектов, как они связаны и какую проблему это снимает. Паттерн не подключается к проекту как библиотека — его каждый раз реализуют под свою задачу. Ниже — три группы паттернов, таблица «задача -> паттерн» и один запускаемый пример на Java с Singleton, Builder, Strategy и Observer. Код проверен на Java 25 (OpenJDK 25.0.4, сентябрь 2026), но работает и на Java 17+.

Паттерн, библиотека, алгоритм: в чем разница

Понятие Что это Пример в Java
Алгоритм Последовательность шагов для вычисления результата Бинарный поиск в Arrays.binarySearch
Библиотека Готовый код, который вызывают java.util, Jackson
Паттерн Схема ролей и связей между классами, код пишешь сам Strategy, Observer

Каталог из 23 классических паттернов описан в книге «Design Patterns» (1994) четырех авторов — Гаммы, Хелма, Джонсона и Влиссидеса, поэтому их называют паттернами GoF (Gang of Four).

Три группы паттернов GoF

Группа Решает Паттерны GoF
Порождающие (5) Как и кем создаются объекты Singleton, Factory Method, Abstract Factory, Builder, Prototype
Структурные (7) Как объекты и классы собираются в более крупные структуры Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Поведенческие (11) Как объекты распределяют обязанности и общаются Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

Многие из них уже встроены в JDK. Интерфейс Comparator — это Strategy: способ сравнения передается в sort снаружи. BufferedReader, оборачивающий Reader, — Decorator. java.util.Iterator — Iterator.

Как выбрать паттерн: от задачи, а не от каталога

Паттерн оправдан, когда проблема уже есть или точно появится:

Задача Что взять Когда не нужно
У объекта много необязательных параметров, конструктор разрастается Builder 2-3 поля — хватит конструктора или record
Алгоритм должен меняться в зависимости от условий Strategy (в Java часто просто лямбда) Вариант один и не меняется
Несколько частей системы реагируют на одно событие Observer Реакция одна и известна заранее
Нужен ровно один экземпляр на приложение Singleton Можно передать объект параметром или через DI-контейнер
Чужой класс с неподходящим интерфейсом Adapter Интерфейс уже подходит

Главный риск — применять паттерны «на вырост»: лишняя абстракция усложняет код без выгоды.

Пример: четыре паттерна в одной программе

Сценарий — заказ: Builder его собирает, Strategy считает доставку, Observer рассылает уведомления, Singleton хранит настройку. Сохрани как Patterns.java и запусти java Patterns.java (так, без javac, можно с Java 11).

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class Patterns {

    // Singleton: enum - один экземпляр на загрузчик классов
    enum AppConfig {
        INSTANCE;
        private final String currency = "RUB";
        String currency() { return currency; }
    }

    // Builder: обязательные поля в конструкторе, проверка в build()
    static final class Order {
        private final String customer;
        private final int amount;
        private final boolean express;

        private Order(Builder b) {
            this.customer = b.customer;
            this.amount = b.amount;
            this.express = b.express;
        }

        static Builder builder(String customer) { return new Builder(customer); }

        static final class Builder {
            private final String customer;
            private int amount;
            private boolean express;

            private Builder(String customer) { this.customer = customer; }
            Builder amount(int amount) { this.amount = amount; return this; }
            Builder express(boolean express) { this.express = express; return this; }
            Order build() {
                if (amount <= 0) throw new IllegalStateException("amount must be > 0");
                return new Order(this);
            }
        }
    }

    // Strategy: алгоритм расчета доставки подменяется снаружи
    interface DeliveryPolicy {
        int cost(Order order);
    }

    // Observer: издатель хранит подписчиков и уведомляет их
    static final class OrderEvents {
        private final List<Consumer<Order>> listeners = new ArrayList<>();
        void subscribe(Consumer<Order> l) { listeners.add(l); }
        void publish(Order o) { for (Consumer<Order> l : listeners) l.accept(o); }
    }

    public static void main(String[] args) {
        Order order = Order.builder("Anna").amount(4500).express(true).build();

        DeliveryPolicy standard = o -> o.amount >= 5000 ? 0 : 300;
        DeliveryPolicy express = o -> 700;
        DeliveryPolicy policy = order.express ? express : standard;

        OrderEvents events = new OrderEvents();
        events.subscribe(o -> System.out.println("email: order for " + o.customer));
        events.subscribe(o -> System.out.println("stats: +" + o.amount));

        System.out.println("delivery = " + policy.cost(order) + " " + AppConfig.INSTANCE.currency());
        events.publish(order);

        try {
            Order.builder("Oleg").build();
        } catch (IllegalStateException e) {
            System.out.println("builder: " + e.getMessage());
        }
    }
}

В консоли появятся четыре строки:

delivery = 700 RUB
email: order for Anna
stats: +4500
builder: amount must be > 0

С express(false) сработает другая стратегия и первая строка станет delivery = 300 RUB: код расчета тот же, меняется выбранный объект.

Singleton: самый спорный из четырех

Singleton гарантирует один экземпляр класса и глобальную точку доступа к нему. Enum-вариант выбран потому, что JVM сама создает INSTANCE один раз и потокобезопасно, а сериализация и рефлексия не создают второй экземпляр. Классическая «ленивая» версия if (instance == null) instance = new ... без синхронизации в многопоточной программе может создать два объекта.

Граница: «один экземпляр» — это один на загрузчик классов, а не на кластер. Главный минус — скрытое глобальное состояние: класс, который сам берет AppConfig.INSTANCE, трудно тестировать с другой настройкой. В Spring синглтоны руками обычно не пишут: бин по умолчанию имеет scope singleton — один экземпляр на контейнер для каждого определения бина (два определения одного класса дадут два объекта), а зависимость приходит через конструктор.

Builder: читаемое создание объекта с проверкой

Builder отделяет пошаговую настройку от готового объекта. .amount(4500).express(true) читается лучше, чем new Order("Anna", 4500, true) с непонятным true. Обязательный customer принимается сразу, а проверка живет в build(), поэтому невалидный заказ не создается — это показала последняя строка вывода. В примере — вариант Builder из «Effective Java» Блоха; в книге GoF у Builder есть еще распорядитель (Director), а один и тот же процесс сборки может давать разные представления результата. Для простых неизменяемых данных с парой полей в современной Java удобнее record.

Strategy: подмена алгоритма без if-цепочек

Strategy выносит изменяемую часть алгоритма в отдельный объект с общим интерфейсом. DeliveryPolicy — функциональный интерфейс (один абстрактный метод), поэтому стратегии записаны лямбдами. С Java 8 многие стратегии — просто лямбды; отдельный класс нужен, если у стратегии есть состояние или несколько методов.

Observer: подписка на события и типичная ошибка

Observer позволяет издателю уведомлять подписчиков, не зная их классов: OrderEvents ничего не знает про email или статистику. Слабое место — изменение списка подписчиков во время рассылки, например подписчик, который отписывается после первого события:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class Broken {
    static final List<Consumer<String>> listeners = new ArrayList<>();

    static void publish(String msg) {
        for (Consumer<String> l : listeners) l.accept(msg);
    }

    static final class Once implements Consumer<String> {
        public void accept(String msg) {
            System.out.println("once: " + msg);
            listeners.remove(this);
        }
    }

    public static void main(String[] args) {
        listeners.add(new Once());
        listeners.add(msg -> System.out.println("log: " + msg));
        publish("order created");
    }
}

Фактический результат — одна строка вместо двух:

once: order created

Второй подписчик молча пропущен: после удаления итератор ArrayList решил, что список кончился. Если подписчиков три, та же программа падает с java.util.ConcurrentModificationException. Исправление — список, безопасный для изменения во время обхода: заменить new ArrayList<>() на new CopyOnWriteArrayList<>() из java.util.concurrent. Тогда вывод:

once: order created
log: order created

CopyOnWriteArrayList копирует массив при каждом изменении — он хорош, когда подписываются редко, а рассылают часто.

Выводы

  • Паттерн — схема ролей и связей для типовой задачи проектирования, а не библиотека: его реализуют под свой код.
  • 23 паттерна GoF делятся на порождающие (5), структурные (7) и поведенческие (11); многие уже есть в JDK (Comparator, Iterator, обертки java.io).
  • Выбирать паттерн от задачи: Builder — при многих параметрах, Strategy — при сменном алгоритме, Observer — при нескольких реакциях на событие.
  • Singleton в Java надежно писать через enum (так советует Блох в «Effective Java»), но чаще лучше передать зависимость явно.
  • В Observer нельзя менять обычный ArrayList подписчиков во время рассылки: подписчик теряется или летит ConcurrentModificationException.

Где применяется / связь с практикой

Освойте тему на практике

Паттерны нужны для кода, который живет годами: сервисы с множеством правил расчета, событийные системы, интеграции. Лучше всего разбирать их на своем проекте — взять участок с разросшимся if или огромным конструктором и отрефакторить. Системно тема разобрана на курсе «Архитектура и шаблоны проектирования»: от SOLID и классических паттернов до архитектурных решений. Курс не привязан к одному языку: задания можно делать на Java, C#, Kotlin, Python и других. Попробовать формат можно на бесплатных открытых уроках Otus.

FAQ

Паттерны проектирования есть только в Java?
Нет, они не зависят от языка, но форма реализации отличается: в языках с функциями первого класса Strategy и Command часто сводятся к передаче функции.

Нужно ли учить все 23 паттерна сразу?
Нет. Для начала хватит Builder, Factory Method, Strategy, Observer, Adapter и Decorator — они часто встречаются в прикладном коде и в JDK; остальные удобнее разбирать, когда встретишь задачу.

Чем Decorator отличается от Proxy?
Структура похожа (обертка с тем же интерфейсом), но цель разная: Decorator добавляет поведение, а Proxy управляет доступом к объекту — откладывает создание, проверяет права, кеширует.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)