Получить доступ

Структурные паттерны

Adapter, Decorator, Proxy и чем они отличаются друг от друга, Facade, Composite, Bridge и Flyweight — на примерах java.io, java.lang.reflect.Proxy и Spring AOP.

О чём эта группа

Структурные паттерны отвечают на вопрос, как собрать объекты в более крупную структуру, не сращивая их намертво. Общий приём у большинства из них один — композиция и делегирование: объект держит ссылку на другой и пропускает вызовы через себя, попутно что-то делая.

Именно из-за общего механизма три паттерна этой группы — Adapter, Decorator и Proxy — постоянно путают: у всех обёртка над объектом и делегирование. Различает их намерение, и на собеседовании спрашивают ровно это.

Adapter

Adapter преобразует интерфейс существующего класса в другой, нужный клиенту. Намерение — совместимость: есть готовый компонент с неподходящей сигнатурой, и переписывать его нельзя (чужая библиотека, legacy).

Признак адаптера — интерфейс на входе и на выходе разный. Именно этим он отличается от декоратора и прокси, которые сохраняют интерфейс.

Примеры в JDK: Arrays.asList(array) выдаёт массив за List; Collections.list(Enumeration) переводит старый Enumeration в ArrayList; InputStreamReader адаптирует байтовый поток к символьному, а OutputStreamWriter — наоборот. В Spring MVC HandlerAdapter позволяет DispatcherServlet единообразно вызывать разные типы обработчиков.

Полезная деталь про Arrays.asList: возвращается адаптер, а не копия — он пишет прямо в исходный массив, имеет фиксированный размер, и add на нём бросает UnsupportedOperationException. Это одновременно и хороший пример адаптера, и уже знакомый пример нарушения LSP.

Decorator

Decorator динамически добавляет объекту новое поведение, сохраняя его интерфейс. Намерение — расширение без наследования: вместо комбинаторного взрыва подклассов (BufferedGzipEncryptedStream) собираем нужную комбинацию во время выполнения.

Канонический пример в Java — пакет java.io. new BufferedInputStream(new GZIPInputStream(new FileInputStream(file))) — три декоратора, каждый добавляет свой аспект (буферизацию, распаковку) и остаётся InputStream. Порядок обёрток при этом значим.

Другие примеры: Collections.unmodifiableList и synchronizedList возвращают декораторы, добавляющие ограничение и синхронизацию; HttpServletRequestWrapper из сервлетного API существует ровно для декорирования запроса в фильтрах.

Ключевые свойства для ответа: интерфейс сохраняется, декораторы вкладываются друг в друга, состав определяет клиент в рантайме, и декоратор добавляет поведение вокруг вызова, а не подменяет объект.

// Декораторы java.io: интерфейс тот же, поведение наращивается.
try (InputStream in = new BufferedInputStream(
                          new GZIPInputStream(
                              new FileInputStream(path)))) {
    ...
}

// Декоратор из Collections: добавляет ограничение, интерфейс прежний.
List<String> readOnly = Collections.unmodifiableList(list);

Proxy и чем он отличается от декоратора

Proxy подставляется вместо реального объекта и контролирует доступ к нему, сохраняя интерфейс. Разновидности: виртуальный (отложить дорогое создание), защитный (проверить права), удалённый (спрятать сетевой вызов), кэширующий.

Разница с декоратором тонкая, и её стоит проговорить именно так: у обоих один интерфейс с оригиналом, но декоратор добавляет поведение к объекту, который ему передал клиент, а прокси управляет доступом к объекту, чьим жизненным циклом обычно распоряжается сам — клиент может вообще не знать, что за прокси стоит реальный объект. Декоратор про «и ещё вот это», прокси про «пущу или не пущу, и когда создам».

Примеры в экосистеме. java.lang.reflect.Proxy создаёт динамический прокси к набору интерфейсов во время выполнения. Hibernate возвращает прокси для ленивых ассоциаций — обращение к полю инициирует подгрузку, а вне сессии даёт LazyInitializationException. Spring AOP заворачивает бины в прокси: @Transactional и @Cacheable работают именно так.

Отсюда знаменитая ловушка Spring, которую любят спрашивать: вызов метода с @Transactional изнутри того же класса (self-invocation) идёт мимо прокси, поэтому аннотация не срабатывает. И вторая: JDK-прокси требует интерфейса, поэтому для классов без интерфейсов Spring использует CGLIB, а такие прокси не могут проксировать final-классы и final-методы.

Facade, Composite, Bridge, Flyweight

Facade даёт простой интерфейс к сложной подсистеме. В отличие от адаптера, он не подгоняет один интерфейс под другой, а скрывает целую группу за упрощённым фасадом. Примеры на слуху: SLF4J — буквально Simple Logging Facade for Java, прослойка над реальными реализациями логирования; JdbcTemplate прячет ритуал JDBC (открыть соединение, подготовить statement, обработать исключения, закрыть ресурсы).

Composite позволяет обращаться к дереву объектов и к отдельному листу одинаково — через общий интерфейс. Пример: узлы UI-контейнеров, файловые деревья, а в Spring Boot — CompositeHealthContributor, где здоровье приложения складывается из вложенных индикаторов, и для клиента составной индикатор ничем не отличается от простого.

Bridge разделяет абстракцию и реализацию, чтобы они менялись независимо. Наглядный пример — JDBC: java.sql.Driver и DriverManager задают абстракцию, а конкретные драйверы поставляют реализации; ваш код к драйверам не привязан. Похоже устроен SLF4J: API отдельно, binding отдельно.

Flyweight экономит память, разделяя одинаковые неизменяемые экземпляры. В JDK это кэш Integer.valueOf на -128..127, пул строковых литералов и Boolean.TRUE/FALSE. Обязательное условие — неизменяемость разделяемых объектов, иначе разделение превращается в общую изменяемую переменную.

Как отвечать на собеседовании

Главный вопрос этой группы — «чем отличаются Adapter, Decorator и Proxy». Отвечайте через намерение и интерфейс: адаптер меняет интерфейс (совместимость), декоратор сохраняет интерфейс и добавляет поведение (расширение), прокси сохраняет интерфейс и контролирует доступ (управление). Механизм у всех трёх один — композиция и делегирование, поэтому по коду они похожи, и различать надо по цели.

Приводите примеры из JDK: java.io — декораторы, Arrays.asList и InputStreamReader — адаптеры, java.lang.reflect.Proxy и Hibernate lazy loading — прокси, SLF4J и JdbcTemplate — фасады.

Сильный ответ дотягивает до практических последствий. Про прокси — self-invocation в Spring и ограничения CGLIB с final. Про декораторы java.io — что порядок обёрток значим и что буферизацию ставят снаружи. Про Flyweight — ловушку == на Integer за пределами кэша.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем