Какие паттерны проектирования вы знаете? Приведите примеры их использования.

Ответ

Паттерны проектирования — это типовые, проверенные решения часто встречающихся проблем в проектировании ПО. Они делятся на три основные категории.

1. Порождающие паттерны (Creational)

Управляют процессом создания объектов, делая его более гибким и независимым.

  • Singleton (Одиночка): Гарантирует, что у класса существует только один экземпляр, и предоставляет глобальную точку доступа к нему.
    public class Logger { // Пример логгера
        private static volatile Logger instance;
        private Logger() {} // Приватный конструктор
        public static Logger getInstance() {
            if (instance == null) { // Double-Checked Locking для потокобезопасности
                synchronized (Logger.class) {
                    if (instance == null) {
                        instance = new Logger();
                    }
                }
            }
            return instance;
        }
        public void log(String message) { /* ... */ }
    }
    // Использование: Logger.getInstance().log("Message");
  • Factory Method (Фабричный метод): Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта.
  • Abstract Factory (Абстрактная фабрика): Создает семейства связанных объектов без указания их конкретных классов.
  • Builder (Строитель): Позволяет создавать сложные объекты пошагово, отделяя конструирование от представления. Полезен для объектов со множеством необязательных параметров.
    public class Computer {
        private final String CPU; // обязательный
        private final String RAM; // обязательный
        private final String storage; // необязательный
        private final String graphicsCard; // необязательный
        // Приватный конструктор, принимающий Builder
        private Computer(Builder builder) {
            this.CPU = builder.CPU;
            this.RAM = builder.RAM;
            this.storage = builder.storage;
            this.graphicsCard = builder.graphicsCard;
        }
        public static class Builder {
            private final String CPU;
            private final String RAM;
            private String storage;
            private String graphicsCard;
            public Builder(String cpu, String ram) { this.CPU = cpu; this.RAM = ram; }
            public Builder storage(String storage) { this.storage = storage; return this; }
            public Builder graphicsCard(String gpu) { this.graphicsCard = gpu; return this; }
            public Computer build() { return new Computer(this); }
        }
    }
    // Использование: Computer pc = new Computer.Builder("Intel i7", "16GB").storage("1TB SSD").build();

2. Структурные паттерны (Structural)

Объединяют классы и объекты в более крупные структуры.

  • Adapter (Адаптер): Позволяет объектам с несовместимыми интерфейсами работать вместе. Преобразует интерфейс одного класса в интерфейс, ожидаемый клиентом.
    // Старая система
    public class LegacyPrinter {
        public void printDocument(String text) { /* печать */ }
    }
    // Новый интерфейс, который требуется клиенту
    public interface ModernPrinter {
        void print(String content);
    }
    // Адаптер
    public class PrinterAdapter implements ModernPrinter {
        private LegacyPrinter legacyPrinter;
        public PrinterAdapter(LegacyPrinter printer) { this.legacyPrinter = printer; }
        @Override
        public void print(String content) {
            legacyPrinter.printDocument(content); // Адаптация вызова
        }
    }
  • Decorator (Декоратор): Динамически добавляет объекту новые обязанности, являясь гибкой альтернативой наследованию для расширения функциональности.
  • Proxy (Заместитель): Предоставляет объект-заменитель, который контролирует доступ к другому объекту (ленивая инициализация, кеширование, защита).
  • Facade (Фасад): Предоставляет простой интерфейс к сложной подсистеме, скрывая ее детали.

3. Поведенческие паттерны (Behavioral)

Решают задачи эффективного взаимодействия и распределения ответственности между объектами.

  • Observer (Наблюдатель): Определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически. Основа event-систем.
    public interface Observer {
        void update(String event);
    }
    public class ConcreteObserver implements Observer {
        private String name;
        @Override
        public void update(String event) { System.out.println(name + " received: " + event); }
    }
    public class Subject {
        private List<Observer> observers = new ArrayList<>();
        public void addObserver(Observer o) { observers.add(o); }
        public void notifyObservers(String event) {
            for (Observer o : observers) { o.update(event); }
        }
    }
  • Strategy (Стратегия): Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет изменять алгоритм независимо от клиента, который его использует.
  • Command (Команда): Инкапсулирует запрос как объект, позволяя параметризовать клиентов с различными запросами, ставить запросы в очередь или поддерживать отмену операций.

Ответ 18+ 🔞

Давай-ка я тебе про эти паттерны расскажу, как есть, без этой академической суеты. Сидишь ты такой, пишешь код, и тут бац — проблема, которую уже до тебя миллион раз решали. Вот эти решения, отточенные до блеска, и есть паттерны. Не магия, а просто опыт, упакованный в удобные коробочки. И их, этих коробочек, всего три главных стопки.

1. Порождающие (Creational) — они про то, как объекты на свет появляются

Их задача — отвязать процесс создания объекта от того, кто его использует. Чтобы не пришлось везде new ConcreteClass() вписывать, а то потом, когда ConcreteClass поменяется, тебе волосы на жопе выпадут от правок.

  • Singleton (Одиночка): Самый знаменитый, самый скандальный, все его ненавидят, но все используют. Гарантирует, что у класса будет один-единственный экземпляр на всю программу. Глобальная точка доступа, да. Часто для логгеров, подключений к базе. Главное — сделать его потокобезопасным, а то будет пиздец.
    public class Logger { // Допустим, логгер
        private static volatile Logger instance; // volatile, чтоб не накосячить с потоками
        private Logger() {} // Конструктор приватный! Чтобы с улицы не вызвали
        public static Logger getInstance() {
            if (instance == null) { // Первая проверка (для скорости)
                synchronized (Logger.class) { // Синхронизация только при первом создании
                    if (instance == null) { // Вторая проверка (Double-Checked Locking)
                        instance = new Logger(); // Вот он, момент истины
                    }
                }
            }
            return instance;
        }
        public void log(String message) { /* ... */ }
    }
    // Используется везде одинаково: Logger.getInstance().log("Всё пропало!");
  • Factory Method (Фабричный метод) & Abstract Factory (Абстрактная фабрика): Первый — делегирует создание объекта наследникам. Второй — создаёт целые семейства связанных объектов, чтобы, например, кнопки, поля ввода и чекбоксы в программе выглядели в одном стиле (Windows или macOS), а не как попало.
  • Builder (Строитель): О, это шедевр, когда у объекта куча полей, и половина из них необязательные. Вместо того чтобы делать конструктор с двадцатью параметрами или кучу перегруженных версий, ты строишь объект по кирпичику. Красота, ядрёна вошь!

    public class Computer {
        private final String CPU; // Обязательно
        private final String RAM; // Обязательно
        private final String storage; // Опционально
        private final String graphicsCard; // Опционально
    
        // Конструктор приватный, работает только с Builder'ом
        private Computer(Builder builder) {
            this.CPU = builder.CPU;
            this.RAM = builder.RAM;
            this.storage = builder.storage;
            this.graphicsCard = builder.graphicsCard;
        }
    
        // Сам Builder, обычно как static nested class
        public static class Builder {
            private final String CPU; // Обязательные поля тут тоже final
            private final String RAM;
            private String storage;
            private String graphicsCard;
    
            // Конструктор Builder'а принимает только обязательное
            public Builder(String cpu, String ram) { this.CPU = cpu; this.RAM = ram; }
    
            // Методы для опциональных полей возвращают самого себя (this)
            public Builder storage(String storage) { this.storage = storage; return this; }
            public Builder graphicsCard(String gpu) { this.graphicsCard = gpu; return this; }
    
            // Финальный метод сборки
            public Computer build() { return new Computer(this); }
        }
    }
    // Использование — просто песня:
    // Computer pc = new Computer.Builder("Intel i7", "16GB").storage("1TB SSD").graphicsCard("RTX 4090").build();

2. Структурные (Structural) — про то, как объекты собираются в кучки

Как из лего собрать что-то большое и работающее, не переделывая сами кирпичики.

  • Adapter (Адаптер): Классика жанра. Есть у тебя старая библиотека или класс с интерфейсом, который никуда не годится. Переписать нельзя, использовать напрямую — больно. Берёшь адаптер, оборачиваешь эту старую хуйню, и она начинает говорить на понятном языке. Как переходник для евро-розетки.
    // Допустим, есть старый принтер из 90-х
    public class LegacyPrinter {
        public void printDocument(String text) { /* печатает с треском */ }
    }
    // А новая система хочет работать по современному интерфейсу
    public interface ModernPrinter {
        void print(String content);
    }
    // Адаптер: снаружи ModernPrinter, внутри работает старый LegacyPrinter
    public class PrinterAdapter implements ModernPrinter {
        private LegacyPrinter legacyPrinter;
        public PrinterAdapter(LegacyPrinter printer) { this.legacyPrinter = printer; }
        @Override
        public void print(String content) {
            // Вот тут и происходит магия адаптации
            legacyPrinter.printDocument(content);
        }
    }
  • Decorator (Декоратор): Хочешь добавить функциональность объекту, но наследование — говно, потому что классов будет овердохуища. Декоратор оборачивает объект и добавляет своё поведение до или после вызова оригинального метода. Как лук в бургере: добавил слой — получил новый вкус, не трогая котлету.
  • Proxy (Заместитель): Объект-посредник. Может откладывать создание тяжёлого объекта (ленивая загрузка), кешировать результаты, контролировать доступ. Стоит между клиентом и реальным объектом, делает вид, что он и есть этот объект, но при этом может творить свою логику.
  • Facade (Фасад): Когда у тебя сложная подсистема из десятка классов, а клиенту нужно сделать одно простое действие. Фасад — это такая красивая дверь с кодовым замком, за которой скрывается ад из проводов, серверов и уставших админов. Клиент нажал одну кнопку — фасад внутри сам позвал десять нужных методов в правильном порядке.

3. Поведенческие (Behavioral) — про то, как объекты общаются и кто за что отвечает

Тут уже про алгоритмы, про распределение обязанностей, чтобы один объект не тянул на себе всё, как тот самый Герасим.

  • Observer (Наблюдатель): Мега-важная штука, основа всех событийных моделей. Есть один объект (Subject), у которого есть состояние. И есть куча других объектов (Observers), которые хотят знать, когда это состояние меняется. Subject не дергает их всех по имени, а просто кричит: «Эй, кто подписан — состояние изменилось!». Все наблюдатели получают уведомление и делают что надо. Рассылка уведомлений, реакция на действия пользователя — везде он.
    // Наблюдатель
    public interface Observer {
        void update(String event); // Метод, который вызовет Subject
    }
    // Конкретный наблюдатель (например, окно уведомлений)
    public class ConcreteObserver implements Observer {
        private String name;
        @Override
        public void update(String event) {
            System.out.println(name + " received event: " + event); // Среагировал!
        }
    }
    // Subject (тот, за кем наблюдают)
    public class Subject {
        private List<Observer> observers = new ArrayList<>();
        public void addObserver(Observer o) { observers.add(o); } // Подписка
        public void notifyObservers(String event) {
            // Прошёлся по всем и уведомил
            for (Observer o : observers) { o.update(event); }
        }
    }
  • Strategy (Стратегия): Позволяет менять алгоритм на лету. Есть контекст (например, класс навигатора) и семейство алгоритмов (маршрут пешком, на машине, на общественном транспорте). Вместо if-else или switch-case ты просто подсовываешь контексту нужную стратегию, и он её выполняет. Чисто, расширяемо, красиво.
  • Command (Команда): Инкапсулирует запрос в объект. Вместо прямого вызова метода ты создаёшь объект-команду, в котором уже есть всё необходимое: кто исполнитель и что делать. Такие команды можно складывать в очередь, откладывать выполнение, логировать и, что самое важное — отменять. Редакторы, очереди задач — без него никуда.

Вот примерно и всё. Не зубри их как стихи, а смотри на задачу. Когда упрёшься в проблему, вспомни: «Ага, тут похоже на Адаптер» или «Да тут Strategy просится!». Со временем они станут как родные, и ты сам начнёшь материться, когда увидишь, как кто-то их не использует там, где они орут и просятся.