Паттерн проектирования — это описание типового решения повторяющейся задачи проектирования: какие роли есть у классов и объектов, как они связаны и какую проблему это снимает. Паттерн не подключается к проекту как библиотека — его каждый раз реализуют под свою задачу. Ниже — три группы паттернов, таблица «задача -> паттерн» и один запускаемый пример на 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 управляет доступом к объекту — откладывает создание, проверяет права, кеширует.



