Ответ
Паттерны проектирования — это типовые, проверенные решения часто встречающихся проблем в проектировании ПО. Они делятся на три основные категории.
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 просится!». Со временем они станут как родные, и ты сам начнёшь материться, когда увидишь, как кто-то их не использует там, где они орут и просятся.