О чём эта группа
Поведенческие паттерны описывают, как объекты распределяют обязанности и общаются между собой. Если порождающие про «как создать», а структурные про «как собрать», то эти — про «кто что делает и в каком порядке».
Отдельная особенность этой группы в современной Java: часть паттернов схлопнулась до языковых средств. Strategy, Command и Observer, которые в 1994 году требовали иерархии классов, сегодня выражаются лямбдой и функциональным интерфейсом. Умение это заметить — заметный плюс на интервью.
Strategy
Strategy выносит взаимозаменяемые алгоритмы за общий интерфейс, чтобы клиент выбирал нужный, не меняясь сам. Это рабочая лошадь OCP: новый алгоритм — новая реализация, а не новая ветка в switch.
Канонический пример в JDK — Comparator: сортировка одна, а порядок задаётся передаваемой стратегией. Туда же RejectedExecutionHandler в ThreadPoolExecutor (что делать с задачей при переполнении очереди — AbortPolicy, CallerRunsPolicy и другие) и ThreadFactory.
В современной Java стратегия чаще всего — это функциональный интерфейс и лямбда: отдельный класс на алгоритм нужен, только когда у стратегии есть состояние, зависимости или её нужно именовать и тестировать отдельно.
Идиома Spring, которую полезно назвать: внедрить List<PaymentHandler> или Map<String, PaymentHandler> и выбирать реализацию по ключу. Контейнер соберёт все бины, и добавление нового способа оплаты не потребует правок в диспетчере.
// Strategy как функциональный интерфейс.
list.sort(Comparator.comparing(User::lastName)
.thenComparing(User::firstName));
// Strategy как набор бинов — идиома Spring и живой OCP.
@Service
class PaymentService {
private final Map<PaymentType, PaymentHandler> handlers; // Spring соберёт все
void pay(Payment p) { handlers.get(p.type()).handle(p); }
}
Template Method
Template Method задаёт скелет алгоритма в базовом классе и оставляет подклассам заполнение отдельных шагов. Порядок шагов фиксирован и защищён — обычно метод-шаблон делают final.
Пример в JDK: AbstractList реализует всю логику списка поверх абстрактных get(int) и size(); InputStream.read(byte[], int, int) реализован через абстрактный read(). В сервлетах HttpServlet.service() разбирает HTTP-метод и вызывает doGet/doPost — вы переопределяете шаг, а не диспетчеризацию.
Ключевое противопоставление, которое любят спрашивать: Template Method против Strategy. Оба параметризуют поведение, но Template Method делает это наследованием на этапе компиляции (жёстко, один шаг — один подкласс, доступ к protected-состоянию базового класса), а Strategy — композицией в рантайме (гибко, стратегию можно подменить и переиспользовать). По принципу «композиция важнее наследования» при прочих равных предпочитают Strategy.
Важный нюанс про JdbcTemplate из Spring: его часто называют примером Template Method, но по механизму это скорее template + callback. Вы не наследуетесь от JdbcTemplate, а передаёте RowMapper или ResultSetExtractor — то есть шаг приходит композицией, а не переопределением. Это удачный пример замены наследования композицией, и его стоит проговорить именно так.
Observer
Observer уведомляет набор подписчиков об изменении состояния субъекта, не связывая их напрямую. Это основа событийной модели.
Историческая деталь, которую полезно знать: java.util.Observer и Observable объявлены deprecated начиная с Java 9 — у них не было ни типобезопасности, ни гарантий порядка, ни поддержки конкурентности. Ссылаться на них как на актуальный пример не стоит; лучше назвать PropertyChangeListener, слушателей Swing или современные механизмы.
Актуальные воплощения: события Spring — ApplicationEventPublisher и @EventListener (плюс @TransactionalEventListener, который откладывает обработку до коммита транзакции); Reactive Streams с парой Publisher/Subscriber, где к базовому Observer добавлен backpressure; брокеры сообщений как тот же паттерн, растянутый на сеть.
Ловушки, которые отличают глубокий ответ. Синхронный слушатель по умолчанию выполняется в том же потоке и в той же транзакции, что и издатель, поэтому его исключение может откатить основную операцию. Подписчики, которых забыли отписать, — классическая утечка памяти (субъект держит на них сильные ссылки). И порядок уведомления в общем случае не гарантирован — полагаться на него нельзя.
Command, Chain of Responsibility, State
Command упаковывает запрос в объект: его можно передать, поставить в очередь, залогировать, отменить. В Java это Runnable и Callable, которые вы отдаёте в ExecutorService: сама задача отделена от того, кто и когда её выполнит. Отмена и повтор — тоже отсюда, а в распределённом варианте команда становится сообщением в брокере.
Chain of Responsibility пропускает запрос по цепочке обработчиков, пока кто-то его не обработает или цепочка не кончится. Самые наглядные примеры — цепочка сервлетных фильтров (каждый решает, вызывать ли chain.doFilter дальше) и фильтр-цепочка Spring Security, где аутентификация, авторизация и CSRF — отдельные звенья. Туда же интерсепторы HTTP-клиентов и middleware.
State меняет поведение объекта при смене его внутреннего состояния — снаружи выглядит так, будто объект сменил класс. Практическое применение — рабочие процессы со статусами (заказ: новый → оплачен → отгружен → закрыт), где вместо разбухающего switch по статусу в каждом методе логика раскладывается по объектам-состояниям, а переходы становятся явными и проверяемыми.
State и Strategy структурно почти неразличимы — оба делегируют работу подставляемому объекту. Разница в намерении и в том, кто управляет подстановкой: стратегию выбирает клиент и она обычно не меняется по ходу, а состояние меняет себя само в ответ на события, и переходы — часть логики.
Iterator, Visitor и современные замены
Iterator даёт последовательный доступ к элементам, не раскрывая внутреннее устройство коллекции. Это java.util.Iterator, и он же — пример Factory Method: каждая коллекция сама создаёт свой итератор. Практическая часть темы (fail-fast, ConcurrentModificationException, weakly consistent итераторы конкурентных коллекций) разобрана в отдельном уроке.
Visitor выносит операцию из иерархии типов в отдельный объект — так добавление новой операции не требует правки всех классов. Цена известна: добавить операцию легко, добавить новый тип в иерархию тяжело (нужно править всех визиторов). Примеры в платформе: java.nio.file.FileVisitor для обхода дерева файлов и ElementVisitor в API обработки аннотаций.
Современная Java даёт визитору прямую альтернативу: sealed-иерархия плюс switch с pattern matching. Набор наследников закрыт, поэтому компилятор проверяет исчерпываемость switch — вы получаете главное преимущество визитора (внешняя операция и контроль полноты) без ритуала с accept/visit. Это хороший пример того, как язык поглощает паттерн.
Общий вывод по группе, который стоит озвучить: паттерн — это словарь для описания решения, а не обязательный к применению код. Половина поведенческих паттернов в современной Java пишется лямбдой в одну строку, и настаивать на классической реализации с иерархией классов там, где хватает функционального интерфейса, — это карго-культ.
Как отвечать на собеседовании
Готовьте ответы парами-противопоставлениями — их спрашивают чаще, чем определения. Strategy против Template Method: композиция в рантайме против наследования на этапе компиляции. Strategy против State: клиент выбирает и не меняет против объект меняет себя сам. Chain of Responsibility против декоратора: звено может прервать цепочку, декоратор всегда делегирует дальше.
На каждый паттерн держите пример из платформы: Comparator — Strategy, HttpServlet.service — Template Method, @EventListener и Reactive Streams — Observer, Runnable в ExecutorService — Command, сервлетные фильтры и Spring Security — Chain of Responsibility, java.util.Iterator — Iterator, FileVisitor — Visitor.
Обязательно упомяните современный срез: java.util.Observable устарел с Java 9, стратегии и команды сегодня чаще лямбды, а sealed-типы с pattern matching заменяют визитор с проверкой исчерпываемости на компиляторе. Это показывает, что вы знаете не книгу 1994 года, а актуальную платформу.
И держите в запасе одну фразу про меру: паттерны — общий словарь для обсуждения дизайна; называть их полезно, натягивать на задачу, где хватает обычного метода, — вредно.