В чем разница между паттернами проектирования «Адаптер», «Фасад» и «Прокси»?

«В чем разница между паттернами проектирования «Адаптер», «Фасад» и «Прокси»?» — вопрос из категории Паттерны, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Паттерны «Адаптер» (Adapter), «Фасад» (Facade) и «Прокси» (Proxy) относятся к структурным паттернам, но решают разные проблемы, связанные с интерфейсами и доступом.

Адаптер (Adapter)

  • Проблема: Несовместимость интерфейсов. Есть клиент, который ожидает интерфейс A, и есть полезный класс с интерфейсом B.
  • Решение: Создать класс-обертку (Адаптер), который преобразует интерфейс класса B в интерфейс A, ожидаемый клиентом.
  • Аналогия: Переходник для розетки.

Пример (Адаптер для старого класса):

// Целевой интерфейс, который ожидает клиент
interface ModernPrinter {
    void printDocument(String text);
}

// Старый, несовместимый класс
class LegacyPrinter {
    void print(String text, int copies) { /*...*/ }
}

// Адаптер
class LegacyPrinterAdapter implements ModernPrinter {
    private LegacyPrinter legacyPrinter;
    public LegacyPrinterAdapter(LegacyPrinter printer) {
        this.legacyPrinter = printer;
    }
    @Override
    public void printDocument(String text) {
        // Адаптируем вызов: преобразуем новый интерфейс в старый
        legacyPrinter.print(text, 1);
    }
}
// Клиентский код работает с ModernPrinter, не зная о LegacyPrinter
ModernPrinter printer = new LegacyPrinterAdapter(new LegacyPrinter());
printer.printDocument("Hello");

Фасад (Facade)

  • Проблема: Слишком сложная подсистема с множеством классов и зависимостей.
  • Решение: Создать единый упрощенный интерфейс (Фасад), который скрывает сложность внутренней подсистемы и предоставляет клиенту только необходимый набор функций.
  • Аналогия: Единый пульт управления для домашнего кинотеатра.

Пример (Фасад для запуска компьютера):

// Сложная подсистема
class CPU { void boot() { /*...*/ } }
class Memory { void loadOS() { /*...*/ } }
class HardDrive { void readBootSector() { /*...*/ } }

// Упрощенный Фасад
class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hdd;
    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hdd = new HardDrive();
    }
    public void start() {
        // Инкапсулирует сложную последовательность запуска
        cpu.boot();
        hdd.readBootSector();
        memory.loadOS();
        System.out.println("Computer ready!");
    }
}
// Клиенту нужно знать только про Фасад
ComputerFacade computer = new ComputerFacade();
computer.start(); // Вместо вызова 3-х отдельных классов в правильном порядке

Прокси (Proxy)

  • Проблема: Необходимость контролировать доступ к объекту или добавлять дополнительную логику при обращении к нему.
  • Решение: Создать объект-заместитель (Прокси), который имеет тот же интерфейс, что и реальный объект, и перехватывает вызовы к нему для добавления своей функциональности.
  • Аналогия: Секретарь, который фильтрует звонки к руководителю.

Пример (Прокси для ленивой загрузки изображения):

interface Image {
    void display();
}

class RealImage implements Image {
    private String filename;
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk(); // Дорогая операция
    }
    private void loadFromDisk() { System.out.println("Loading " + filename); }
    @Override
    public void display() { System.out.println("Displaying " + filename); }
}

class ProxyImage implements Image { // Имеет тот же интерфейс
    private RealImage realImage;
    private String filename;
    public ProxyImage(String filename) { this.filename = filename; }
    @Override
    public void display() {
        // Ленивая инициализация: создаем реальный объект только при необходимости
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display(); // Делегируем вызов реальному объекту
    }
}
// Клиент может использовать Proxy так же, как RealImage
Image image = new ProxyImage("photo.jpg");
// RealImage еще не создан
image.display(); // Создается RealImage и отображается
image.display(); // RealImage уже создан, просто отображается

Сравнительная таблица

Критерий Адаптер (Adapter) Фасад (Facade) Прокси (Proxy)
Основная цель Преобразовать один интерфейс в другой Упростить сложный интерфейс подсистемы Контролировать доступ к объекту (заместитель)
Структурные изменения Меняет интерфейс Предоставляет новый, упрощенный интерфейс Сохраняет исходный интерфейс
Количество объектов Работает обычно с одним несовместимым объектом Работает с целой подсистемой объектов Работает с одним объектом, который он представляет
Время применения Post-factum, для интеграции существующего кода Предварительно, для проектирования удобного слоя доступа Post-factum или изначально, для добавления логики доступа
Типичные сценарии Интеграция сторонних библиотек, работа с legacy-кодом Упрощение работы со сложными фреймворками (например, низкоуровневое API) Ленивая загрузка, кэширование, защита доступа, логирование, удаленные прокси (RPC)