Книге «Design Patterns» банды четырёх уже больше тридцати лет, но читать её как готовый каталог рецептов сегодня не нужно — половина паттернов давно растворилась в языке и фреймворках. Зачем тогда учить? Потому что Spring, Hibernate, любой HTTP-фреймворк разговаривают именно этим словарём. HandlerAdapter, CompositeHealthContributor, DelegatingPasswordEncoder — это не случайные имена, это паттерны GoF в названиях классов. Не зная паттерн, трудно понять, почему класс устроен так, а не иначе.
Ниже — все 23 паттерна по классическим группам. Для каждого: суть в одном предложении, где он уже живёт в готовых инструментах и нужно ли его писать самому.
Порождающие паттерны
Эта группа отвечает на один вопрос: как правильно создавать объекты?
Singleton
Суть: один экземпляр на всё приложение.
Если создавать объект в каждом месте, где он нужен, получается несколько несвязанных копий с разным состоянием. Singleton решает это: объект существует в одном экземпляре, и все обращаются к одному и тому же.
В Spring синглтон реализуется через контейнер — каждый @Service-бин по умолчанию создаётся в одном экземпляре. Это лучше классического GoF-варианта со static getInstance(): контейнерный синглтон внедряется через конструктор и легко подменяется в тестах.
Главное следствие: один экземпляр обслуживает все запросы параллельно, поэтому в singleton-бинах не должно быть изменяемого состояния — иначе возникает гонка потоков.
@Service
public class PricingService {
// один экземпляр на весь контекст приложения
}
@Test
void calculatesPrice() {
var service = new PricingService(); // в тесте создаём через new — просто
}
Prototype
Суть: новый экземпляр при каждом запросе.
Противоположность Singleton: иногда нужен свежий объект для каждой операции, а не общий на всех. В Spring это scope = prototype — каждый getBean() даёт новый объект.
Ловушка: если внедрить prototype-бин в singleton-бин обычным образом, внедрение произойдёт один раз при создании singleton'а — и «новизна» потеряется. Решение — ObjectProvider:
@Service
@RequiredArgsConstructor
public class ReportController {
private final ObjectProvider<ReportBuilder> reportBuilders;
public ReportDto create(ReportRequest request) {
var builder = reportBuilders.getObject(); // каждый раз новый экземпляр
return builder.with(request).build();
}
}
Factory Method
Суть: создание объекта делегируется методу, скрывающему конкретный класс.
Вместо того чтобы везде писать new ConcreteClass(...), вызывающий код работает с интерфейсом, а фабричный метод решает, какую реализацию создать.
В Spring каждый @Bean-метод — это фабричный метод: вызывающий код знает интерфейс, метод решает, какую реализацию сконфигурировать:
@Configuration
public class ClockConfig {
@Bean
@Profile("!integration-test")
Clock clock() {
return Clock.systemUTC();
}
@Bean
@Profile("integration-test")
Clock fixedClock() {
return Clock.fixed(Instant.parse("2026-01-15T10:00:00Z"), ZoneOffset.UTC);
}
}
В прикладном коде статические фабричные методы — хороший способ создавать объекты с валидацией:
public final class Order {
public static Order create(CustomerId customerId, List<OrderLine> lines) {
if (lines.isEmpty()) {
throw new IllegalArgumentException("Заказ должен содержать хотя бы одну позицию");
}
return new Order(OrderId.generate(), customerId, lines, Status.CREATED);
}
}
Abstract Factory
Суть: фабрика, которая создаёт семейство связанных объектов.
Если Factory Method создаёт один объект, Abstract Factory создаёт целую группу объектов, которые должны «сочетаться» друг с другом.
В Spring роль абстрактной фабрики играет BeanFactory и ApplicationContext: по типу выдают готовые объекты, скрывая конкретную реализацию. Какой будет PlatformTransactionManager — JpaTransactionManager или DataSourceTransactionManager — решает конфигурация, не вызывающий код.
В прикладном коде Abstract Factory пишут редко: конфигурацию по профилям (@Profile/@Conditional) покрывает задачу проще.
Builder
Суть: пошаговая сборка сложного объекта с читаемым кодом.
Конструктор с восемью параметрами — источник ошибок: легко перепутать порядок, непонятно, что за что отвечает. Builder даёт именованные методы для каждого поля.
В Spring Builder используется в HttpSecurity (конфигурация безопасности) и во всех HTTP-клиентах. Lombok генерирует Builder автоматически через @Builder:
@Builder
public record OrderSearchQuery(
@Nullable CustomerId customerId,
@Nullable OrderStatus status,
int page,
int size
) {}
var query = OrderSearchQuery.builder()
.status(OrderStatus.PAID)
.page(0)
.size(20)
.build();
Структурные паттерны
Эта группа отвечает на вопрос: как правильно строить связи между объектами?
Adapter
Суть: преобразует один интерфейс в другой, которого ожидает клиент.
Представьте две заглушки разной формы: код ожидает один интерфейс, а внешняя библиотека предлагает другой. Adapter — переходник между ними.
В Spring DispatcherServlet не знает, чем написан обработчик — @RequestMapping-методом или чем-то другим. Он работает с единым HandlerAdapter, который приводит любой стиль к общему контракту.
В прикладном коде Adapter — основа работы с внешними зависимостями: адаптер переводит доменный интерфейс в язык конкретного SDK:
@Component
@RequiredArgsConstructor
public class S3DocumentStorageAdapter implements DocumentStoragePort {
private final S3Client s3Client;
@Override
public DocumentRef store(Document document) {
var key = document.id().value().toString();
s3Client.putObject(b -> b.bucket("documents").key(key),
RequestBody.fromBytes(document.content()));
return new DocumentRef(key);
}
}
Bridge
Суть: абстракция и реализация развиваются независимо.
Классический пример: Resource и ResourceLoader в Spring — одна абстракция «ресурс», независимые реализации под classpath:, file:, https:. Код, читающий ресурс, не меняется при смене источника.
В прикладном коде Bridge в чистом виде почти не встречается — его заменяет связка «интерфейс + внедрение зависимости».
Composite
Суть: группа объектов используется так же, как один объект.
Нужно отправить уведомление одновременно по email и SMS, но вызывающий код не должен знать о деталях. Composite позволяет «завернуть» несколько объектов в один, реализующий тот же интерфейс.
В Spring узнаётся по префиксу Composite: CompositePropertySource, CompositeCacheManager, CompositeHealthContributor в Actuator — несколько источников выглядят как один.
@Component
@Primary
@RequiredArgsConstructor
public class CompositeNotificationAdapter implements NotificationPort {
private final List<NotificationPort> channels;
@Override
public void orderCancelled(Order order) {
channels.forEach(channel -> channel.orderCancelled(order));
}
}
Decorator
Суть: объект оборачивается в обёртку с тем же интерфейсом, добавляющую поведение.
Нужно добавить кеширование к репозиторию, но менять его класс нельзя (или не хочется). Decorator создаёт обёртку с тем же интерфейсом, которая перехватывает вызовы и добавляет нужное поведение.
В Spring это ContentCachingRequestWrapper, TransactionAwareDataSourceProxy, DelegatingSecurityContextExecutor. AOP-прокси для @Transactional — тоже Decorator по сути.
@Component
@Primary
@RequiredArgsConstructor
public class CachingProductRepository implements ProductRepository {
private final JpaProductRepository delegate;
private final Cache cache;
@Override
public Optional<Product> findById(ProductId id) {
return Optional.ofNullable(
cache.get(id, () -> delegate.findById(id).orElse(null))
);
}
}
Facade
Суть: простой интерфейс над сложной подсистемой.
Работа с JDBC напрямую требует: открыть соединение, создать PreparedStatement, выполнить, обработать ResultSet, закрыть всё в правильном порядке. JdbcTemplate скрывает всю эту сложность за одним вызовом.
В Spring все *Template и *Client — это фасады: JdbcTemplate, KafkaTemplate, RestClient. В прикладном коде фасад над внешним SDK — нормальная форма адаптера: один метод может скрывать три вызова стороннего API, повторные попытки и преобразование ошибок.
@Component
@RequiredArgsConstructor
public class PaymentGatewayAdapter implements PaymentPort {
private final PaymentSdkClient sdkClient;
@Override
public PaymentResult charge(Order order, PaymentMethod method) {
var request = sdkClient.newRequest()
.amount(order.total().amount())
.currency(order.total().currency().code())
.method(method.token())
.build();
var response = sdkClient.submit(request);
return PaymentResult.of(response.transactionId(), response.status());
}
}
Flyweight
Суть: разделяемые неизменяемые объекты вместо тысяч одинаковых копий.
Если создавать по объекту на каждое слово в тексте, память закончится быстро. Flyweight разделяет объекты с одинаковым содержимым — один экземпляр на значение.
В Java это кеш Integer.valueOf(-128..127) и интернирование строк. Во фреймворках — внутренние кеши метаданных аннотаций и типов.
В прикладном коде Flyweight почти никогда не пишут руками. Его идею несут неизменяемые value objects и константы: Currency.RUB один на всё приложение именно потому, что неизменяем.
Proxy
Суть: объект-заместитель контролирует доступ к реальному объекту.
Прокси перехватывает вызовы и делает что-то до или после: открывает транзакцию, проверяет права, кеширует результат.
Это паттерн номер один во всём Spring. JDK dynamic proxy и CGLIB — механизм, на котором работают @Transactional, @Cacheable, @Async, @PreAuthorize. Аннотация на методе — инструкция контейнеру: «заверни бин в прокси».
Отсюда и классические ловушки: вызов метода из того же класса (this.method()) обходит прокси и аннотации не срабатывают — подробно в статье про AOP.
@Service
public class TransferService {
@Transactional // Spring создаёт прокси, который открывает транзакцию
public void transfer(AccountId from, AccountId to, Money amount) {
// вызывающий код получает прокси, а не этот класс напрямую
}
}
Поведенческие паттерны
Эта группа отвечает на вопрос: как организовать взаимодействие объектов?
Chain of Responsibility
Суть: запрос идёт по цепочке обработчиков, пока кто-то его не обработает.
HTTP-запрос нужно сначала проверить на аутентификацию, потом на CSRF, потом на авторизацию, и каждый шаг может остановить обработку. Вместо одного огромного метода — цепочка независимых обработчиков.
В Spring это SecurityFilterChain — эталон паттерна. Та же механика у MVC-интерсепторов и цепочек обработчиков исключений.
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
MDC.put("traceId", resolveTraceId(request));
try {
chain.doFilter(request, response); // передаём дальше по цепочке
} finally {
MDC.remove("traceId");
}
}
}
Command
Суть: операция упакована в объект — её можно передать, отложить, поставить в очередь.
Обычно вызов метода мгновенный и безымянный. Command превращает операцию в объект с данными — его можно передать в другой поток, отложить, залогировать, отменить.
В Java это Runnable/Callable, уходящие в пул потоков. В Spring — @Scheduled-задачи и @Async-методы.
В прикладном коде Command — структурная основа разделения «что делать» и «как делать»: один объект несёт данные операции, другой её исполняет.
public record CancelOrderCommand(OrderId orderId, CancelReason reason) {}
@Component
@RequiredArgsConstructor
public class CancelOrderHandler {
private final OrderRepository orderRepository;
@Transactional
public void handle(CancelOrderCommand command) {
var order = orderRepository.findById(command.orderId()).orElseThrow();
order.cancel(command.reason());
orderRepository.save(order);
}
}
Interpreter
Суть: язык с грамматикой и интерпретатор выражений на нём.
В Spring это SpEL (Spring Expression Language): выражения в @PreAuthorize("hasRole('ADMIN')"), @Cacheable(key = "#id"), @Value("#{systemProperties['user.home']}").
В собственном коде создавать мини-языки не стоит: строковые выражения не проверяются компилятором, ломаются при рефакторинге и усложняют отладку.
Iterator
Суть: последовательный доступ к элементам без раскрытия внутренней структуры.
Паттерн давно растворился в языке: Iterable/Iterator, for-each, Stream. Spring Data добавляет Page и Slice для постраничных выборок.
Реализовывать Iterator руками не приходится. Единственное близкое решение — отдавать из коллекций агрегата неизменяемые представления.
Mediator
Суть: объекты общаются через посредника, не зная друг о друге.
Если один сервис напрямую вызывает другой, они тесно связаны: изменение одного ломает другой. Mediator убирает прямую зависимость — объекты публикуют события, и кто хочет — подписывается.
В Spring это ApplicationEventPublisher: бин публикует событие, слушатели реагируют, и никто ни с кем не связан напрямую. DispatcherServlet — тоже медиатор MVC.
@Component
@RequiredArgsConstructor
public class CancelOrderHandler {
private final ApplicationEventPublisher events;
public void handle(CancelOrderCommand command) {
// ... логика отмены ...
events.publishEvent(new OrderCancelled(command.orderId()));
}
}
@Component
public class RefundListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(OrderCancelled event) {
// другой модуль, не знает об обработчике отмены
}
}
Memento
Суть: снимок состояния объекта для последующего отката.
Savepoint в транзакциях — чистый Memento: TransactionStatus.createSavepoint() фиксирует точку, rollbackToSavepoint() откатывает к ней.
В прикладном коде почти никогда не нужен: откат состояния — работа транзакции базы данных, история изменений — отдельная таблица аудита или event sourcing.
Observer
Суть: подписчики получают уведомления об изменении состояния издателя.
Нужно отправить письмо при отмене заказа. Можно вызвать emailService.send(...) прямо в бизнес-логике — но тогда бизнес-логика знает об email-сервисе. Observer разделяет их: бизнес-логика публикует событие, email-сервис подписывается.
В Spring это @EventListener и @TransactionalEventListener. Важный нюанс: если слушатель с внешними эффектами (письмо, SMS) вешается без привязки к транзакции, уведомление может уйти по откаченной транзакции. Правильно — phase = TransactionPhase.AFTER_COMMIT.
@Component
public class OrderNotificationListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(OrderCancelled event) {
notificationPort.orderCancelled(event.orderId());
}
}
State
Суть: поведение объекта меняется при смене его внутреннего состояния.
Заказ в статусе CREATED можно отменить. Заказ в статусе DELIVERED — нет. Логику переходов можно разложить в отдельные классы-состояния, но для большинства задач достаточно проверок в методах самого объекта:
public class Order {
public void cancel(CancelReason reason) {
if (status != Status.PAID && status != Status.CREATED) {
throw new IllegalOrderStateException(id, status, "cancel");
}
this.status = Status.CANCELLED;
registerEvent(new OrderCancelled(id, reason));
}
}
Классический State с отдельным классом на каждое состояние оправдан при очень большой машине состояний. Для обычного объекта с несколькими статусами хватает enum и проверок переходов.
Strategy
Суть: семейство алгоритмов за общим интерфейсом, выбираемых в зависимости от ситуации.
Скидки для разных категорий покупателей: можно написать switch с условиями, а можно объявить интерфейс DiscountPolicy и создать по реализации на каждую категорию. Добавление новой категории — новый класс, а не правка switch.
В Spring Strategy повсюду: PasswordEncoder (выбирает алгоритм хеширования), PlatformTransactionManager, AuthenticationProvider, ContentNegotiationStrategy.
public interface DiscountPolicy {
boolean supports(Order order);
Money apply(Order order);
}
@Component
@RequiredArgsConstructor
public class DiscountService {
private final List<DiscountPolicy> policies;
public Money calculateDiscount(Order order) {
return policies.stream()
.filter(p -> p.supports(order))
.map(p -> p.apply(order))
.reduce(Money.ZERO, Money::add);
}
}
Template Method
Суть: скелет алгоритма в базовом классе, изменяемые шаги — в наследниках.
Фильтр запроса всегда должен выполниться один раз, даже если в цепочке его вызовут дважды. Базовый класс OncePerRequestFilter берёт это на себя, оставляя наследнику только содержательную часть:
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(...) {
// пишем только бизнес-логику фильтра, гарантию «один раз» берёт суперкласс
}
}
Современный вариант — передавать шаг как функцию, а не создавать наследника. JdbcTemplate так и делает: скелет (открыть соединение → выполнить → закрыть) постоянный, переменный шаг (SQL-запрос) передаётся лямбдой.
Visitor
Суть: новая операция над структурой объектов без изменения их классов.
Есть иерархия типов PaymentMethod: Card, Sbp, Cash. Нужно считать комиссию по-разному для каждого типа, не добавляя метод fee() в каждый класс. Visitor добавляет операцию снаружи.
В современных языках Visitor вытеснил pattern matching:
// Java 21: sealed + pattern matching вместо Visitor
public sealed interface PaymentMethod permits Card, Sbp, Cash {}
public BigDecimal fee(PaymentMethod method) {
return switch (method) {
case Card card -> card.amount().multiply(new BigDecimal("0.02"));
case Sbp sbp -> BigDecimal.ZERO;
case Cash cash -> new BigDecimal("50");
};
}
Все 23 паттерна: быстрая сводка
| Паттерн | Где встречается в готовых инструментах | Нужен ли в своём коде |
|---|---|---|
| Singleton | Дефолтный scope бина в Spring | Не писать руками — это работа контейнера |
| Prototype | Scope prototype, ObjectProvider | Редко; чаще достаточно локальной переменной |
| Factory Method | @Bean-методы, FactoryBean<T> | Да — статические фабричные методы для создания объектов с валидацией |
| Abstract Factory | BeanFactory, ApplicationContext | Не нужен — конфигурацию собирает @Profile |
| Builder | HTTP-клиенты, HttpSecurity в Spring Security | Да — через @Builder от Lombok или вручную |
| Adapter | HandlerAdapter, MessageListenerAdapter | Да — адаптеры к внешним зависимостям |
| Bridge | Resource/ResourceLoader, logging SPI | Почти никогда — «интерфейс + DI» покрывает |
| Composite | CompositePropertySource, CompositeHealthContributor | Да — когда нужно несколько получателей за одним интерфейсом |
| Decorator | Request-wrappers, TransactionAwareDataSourceProxy | Да — обёртки над репозиториями; сначала проверьте встроенные механизмы |
| Facade | JdbcTemplate, KafkaTemplate, RestClient | Да — адаптер-фасад над чужим SDK |
| Flyweight | Кеши метаданных во фреймворках, Integer.valueOf | Почти никогда — идею несут неизменяемые value objects |
| Proxy | AOP: @Transactional, @Cacheable, @Async, @PreAuthorize | Не писать — использовать встроенные механизмы AOP |
| Chain of Responsibility | SecurityFilterChain, MVC-интерсепторы | Редко — хватает готовых цепочек фреймворка |
| Command | Runnable + пул потоков, @Scheduled, @Async | Да — разделение «что» и «как» в обработчиках операций |
| Interpreter | SpEL: @PreAuthorize, @Cacheable(key=...), @Value | Не изобретать собственных языков выражений |
| Iterator | Iterable, Stream, Page/Slice в Spring Data | Растворился в языке |
| Mediator | ApplicationEventPublisher, DispatcherServlet | Да — события вместо прямых вызовов между модулями |
| Memento | Savepoint в транзакциях | Почти никогда — откат делает транзакция базы данных |
| Observer | @EventListener, @TransactionalEventListener | Да — доменные события, постоянно |
| State | Spring Statemachine для сложных случаев | Да, в облегчённой форме — enum + проверки переходов |
| Strategy | PasswordEncoder, PlatformTransactionManager | Да — вместо разрастающихся switch |
| Template Method | OncePerRequestFilter, AbstractRoutingDataSource | Колбэк-вариант предпочтительнее наследования |
| Visitor | ASM в component scanning | Вытеснен pattern matching (Java 21+) |
Из 23 паттернов в прикладном коде регулярно пишут семь–восемь: Adapter, Strategy, Observer, Command, Decorator, Composite, Factory Method и State. Ещё столько же используют каждый день в готовом виде, не замечая: Proxy, Singleton, Builder, Facade, Template Method, Chain of Responsibility. Остальные — словарь для чтения чужого кода.
Коротко
- Паттерны GoF — не рецепты для копирования, а словарь: именно им разговаривают фреймворки в названиях классов.
- Proxy — паттерн номер один в Spring:
@Transactional,@Cacheable,@Asyncработают через него. - Strategy — главный инструмент против разрастающихся
switch. - Observer — стандартный способ отделить побочные действия (письмо, метрика) от бизнес-логики.
- Adapter — основа работы с внешними зависимостями: домен знает интерфейс, адаптер знает конкретный SDK.
- Singleton не пишут вручную со
static getInstance()— это работа DI-контейнера. - Decorator добавляет поведение без наследования — но сначала проверьте, нет ли аннотации во фреймворке.
- Из 23 паттернов регулярно пишут руками ~7; остальные живут в готовых инструментах.
Что почитать дальше
- SOLID на примерах — принципы, ради которых эти паттерны существуют.
- GRASP на примерах — какому классу отдать ответственность, прежде чем выбирать паттерн.
- Spring AOP — как устроен Proxy, паттерн номер один Spring.
- DI/IoC, bean scopes — Singleton и Prototype как scope контейнера.
- Spring Events — Observer и Mediator в работе.