← назад к разделу

Книге «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: по типу выдают готовые объекты, скрывая конкретную реализацию. Какой будет PlatformTransactionManagerJpaTransactionManager или 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Не писать руками — это работа контейнера
PrototypeScope prototype, ObjectProviderРедко; чаще достаточно локальной переменной
Factory Method@Bean-методы, FactoryBean<T>Да — статические фабричные методы для создания объектов с валидацией
Abstract FactoryBeanFactory, ApplicationContextНе нужен — конфигурацию собирает @Profile
BuilderHTTP-клиенты, HttpSecurity в Spring SecurityДа — через @Builder от Lombok или вручную
AdapterHandlerAdapter, MessageListenerAdapterДа — адаптеры к внешним зависимостям
BridgeResource/ResourceLoader, logging SPIПочти никогда — «интерфейс + DI» покрывает
CompositeCompositePropertySource, CompositeHealthContributorДа — когда нужно несколько получателей за одним интерфейсом
DecoratorRequest-wrappers, TransactionAwareDataSourceProxyДа — обёртки над репозиториями; сначала проверьте встроенные механизмы
FacadeJdbcTemplate, KafkaTemplate, RestClientДа — адаптер-фасад над чужим SDK
FlyweightКеши метаданных во фреймворках, Integer.valueOfПочти никогда — идею несут неизменяемые value objects
ProxyAOP: @Transactional, @Cacheable, @Async, @PreAuthorizeНе писать — использовать встроенные механизмы AOP
Chain of ResponsibilitySecurityFilterChain, MVC-интерсепторыРедко — хватает готовых цепочек фреймворка
CommandRunnable + пул потоков, @Scheduled, @AsyncДа — разделение «что» и «как» в обработчиках операций
InterpreterSpEL: @PreAuthorize, @Cacheable(key=...), @ValueНе изобретать собственных языков выражений
IteratorIterable, Stream, Page/Slice в Spring DataРастворился в языке
MediatorApplicationEventPublisher, DispatcherServletДа — события вместо прямых вызовов между модулями
MementoSavepoint в транзакцияхПочти никогда — откат делает транзакция базы данных
Observer@EventListener, @TransactionalEventListenerДа — доменные события, постоянно
StateSpring Statemachine для сложных случаевДа, в облегчённой форме — enum + проверки переходов
StrategyPasswordEncoder, PlatformTransactionManagerДа — вместо разрастающихся switch
Template MethodOncePerRequestFilter, AbstractRoutingDataSourceКолбэк-вариант предпочтительнее наследования
VisitorASM в 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 в работе.