🧱Creational
1) Singleton
🧭Назначение:
гарантирует, что в системе существует только один экземпляр класса, и предоставляет к нему глобальную точку доступа.
⚙Когда использовать:
– классы для работы с конфигурацией;
– классы логирования;
– пул соединений с БД.
🎯Ключевые преимущества:
- Гарантия единственного экземпляра
Класс сам следит, чтобы был создан только один объект, независимо от числа вызововgetInstance(). - Глобальная точка доступа
Объект доступен в любом месте программы через статический метод — удобно, когда нужно общее состояние (например, настройки). - Ленивая инициализация (lazy initialization)
Экземпляр можно создать только тогда, когда он реально понадобится (экономит ресурсы). - Контроль над доступом к ресурсу
Если, например, Singleton управляет пулом подключений, он может гарантировать безопасный доступ к общему ресурсу. - Совместим с паттерном Factory / Builder
Часто используется вместе с ними, например, фабрика как Singleton, чтобы централизовать создание объектов.
⚠ Недостатки:
- Скрытая глобальная зависимость
Singleton фактически превращается в «глобальную переменную».
Это усложняет тестирование и нарушает принцип инверсии зависимостей (D в SOLID). - Трудности с юнит-тестированием
Если класс напрямую обращается к Singleton, подменить его на mock сложно.
Придётся шаманить с рефлексией или DI. - Проблемы с многопоточностью
Если не реализовать потокобезопасность (synchronized, double-checked locking), можно случайно создать несколько экземпляров. - Жёсткая связанность и нарушение принципа SRP
Класс одновременно и создаёт, и хранит, и управляет собой — три ответственности в одном месте. - Жизненный цикл сложно контролировать
Объект живёт всё время работы программы. Если Singleton что-то держит в памяти — утечка обеспечена.
Пример:
public class Logger {
private static Logger instance;
private Logger() {} // приватный конструктор
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String msg) {
System.out.println(msg);
}
}Использование:
Logger.getInstance().log("Started app...");2) Factory Method
🧭Назначение:
делегирует создание объектов подклассам, не указывая конкретный тип.
⚙Когда использовать:
– Spring Bean Factory, Hibernate SessionFactory;
– создание разных типов объектов с общим интерфейсом.
🎯Ключевые преимущества:
⚠ Недостатки:
Пример. Вариант А.
// Vehicle.java
public abstract class Vehicle {
protected Vehicle() { } // наследники смогут вызвать super()
public abstract void printVehicle();
}// TwoWheeler.java
public class TwoWheeler extends Vehicle {
@Override
public void printVehicle() {
System.out.println("I am two wheeler");
}
}// FourWheeler.java
public class FourWheeler extends Vehicle {
@Override
public void printVehicle() {
System.out.println("I am four wheeler");
}
}// VehicleFactory.java
public abstract class VehicleFactory {
// Фабричный метод — решает, какой Vehicle создать (в подклассах)
protected abstract Vehicle createVehicle();
// Общая логика, использующая продукт
public void deliver() {
Vehicle v = createVehicle();
System.out.print("Delivering vehicle: ");
v.printVehicle();
}
}// TwoWheelerFactory.java (Конкретный создатель)
public class TwoWheelerFactory extends VehicleFactory {
@Override
protected Vehicle createVehicle() {
return new TwoWheeler();
}
}// FourWheelerFactory.java (Конкретный создатель)
public class FourWheelerFactory extends VehicleFactory {
@Override
protected Vehicle createVehicle() {
return new FourWheeler();
}
}// App.java
public class App {
public static void main(String[] args) {
VehicleFactory bikeFactory = new TwoWheelerFactory();
VehicleFactory carFactory = new FourWheelerFactory();
bikeFactory.deliver(); // Delivering vehicle: I am two wheeler
carFactory.deliver(); // Delivering vehicle: I am four wheeler
}
}Пример. Вариант B. Simple Factory Method
// Vehicle.java
public abstract class Vehicle {
protected Vehicle() { }
public abstract void printVehicle();
}// TwoWheeler.java
public class TwoWheeler extends Vehicle {
@Override
public void printVehicle() {
System.out.println("I am two wheeler");
}
}// FourWheeler.java
public class FourWheeler extends Vehicle {
@Override
public void printVehicle() {
System.out.println("I am four wheeler");
}
}// VehicleType.java
public enum VehicleType {
TWO_WHEELER,
FOUR_WHEELER
}// SimpleVehicleFactory.java
public final class SimpleVehicleFactory {
private SimpleVehicleFactory() { }
public static Vehicle create(VehicleType type) {
switch (type) {
case TWO_WHEELER: return new TwoWheeler();
case FOUR_WHEELER: return new FourWheeler();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}// App.java
public class App {
public static void main(String[] args) {
Vehicle v1 = SimpleVehicleFactory.create(VehicleType.TWO_WHEELER);
Vehicle v2 = SimpleVehicleFactory.create(VehicleType.FOUR_WHEELER);
v1.printVehicle(); // I am two wheeler
v2.printVehicle(); // I am four wheeler
}
}3) Abstract Factory
🧭Назначение:
Она предоставляет интерфейс для создания объектов, где каждый конкретный подкласс решает, какие именно конкретные реализации использовать.
⚙Когда использовать:
- UI-библиотеки и фреймворки
Например, Swing, Qt, JavaFX — выбирают подходящую реализацию компонентов под платформу. - Базы данных / ORM
Hibernate или Spring Data создают фабрики для подключения к разным типам баз (PostgreSQL, Oracle и т. д.). - Игровые движки
Для выбора разных семейств объектов (например, монстры + оружие разных рас). - IoC-контейнеры и DI-фреймворки (Spring, Guice)
Вся концепция BeanFactory — это развитие идеи Abstract Factory.
🎯Ключевые преимущества:
- Изоляция конкретных классов от кода клиента.
- Удобное переключение между «семействами» объектов.
- Гарантия совместимости объектов в одном семействе.
⚠ Недостатки:
Если нужно добавить новый тип продукта, то придётся менять все фабрики, потому что каждая обязана поддерживать создание всех видов продуктов.
(Зато добавлять новые семейства — легко.)
Пример:
/**
* Abstract Products
*/
// Button
public interface Button {
void paint();
}
// Checkbox
public interface Checkbox {
void paint();
}/**
* Concrete Products
*/
// Windows
public class WinButton implements Button {
public void paint() {
System.out.println("Rendering a Windows-style button.");
}
}
public class WinCheckbox implements Checkbox {
public void paint() {
System.out.println("Rendering a Windows-style checkbox.");
}
}
// MacOS
public class MacButton implements Button {
public void paint() {
System.out.println("Rendering a macOS-style button.");
}
}
public class MacCheckbox implements Checkbox {
public void paint() {
System.out.println("Rendering a macOS-style checkbox.");
}
}/**
* Abstract Factory
*/
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}/**
* Concrete Factory
*/
public class WinFactory implements GUIFactory {
public Button createButton() {
return new WinButton();
}
public Checkbox createCheckbox() {
return new WinCheckbox();
}
}
public class MacFactory implements GUIFactory {
public Button createButton() {
return new MacButton();
}
public Checkbox createCheckbox() {
return new MacCheckbox();
}
}/**
* Client
*/
public class Application {
private final Button button;
private final Checkbox checkbox;
public Application(GUIFactory factory) {
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
public void render() {
button.paint();
checkbox.paint();
}
}/**
* Demo
*/
public class Demo {
public static void main(String[] args) {
GUIFactory factory;
String os = "mac"; // может быть считано из конфига или окружения
if (os.equalsIgnoreCase("windows")) {
factory = new WinFactory();
} else {
factory = new MacFactory();
}
Application app = new Application(factory);
app.render();
}
}4) Builder
это тот случай, когда объект слишком сложен, чтобы его удобно собирать через конструктор. Он помогает создавать сложные объекты пошагово, при этом код остаётся чистым и читаемым.
🧭Назначение:
Отделить процесс конструирования сложного объекта от его представления,
чтобы один и тот же процесс можно было использовать для создания разных представлений.
Говоря проще — чтобы не мучиться с длинными конструкторами вроде
new Car("V6", 4, true, "red", "automatic", true, "hybrid"),
⚙Когда использовать:
- Когда объект имеет много параметров, особенно опциональных.
- Когда нужно разные комбинации (варианты конфигурации) сложного объекта.
- Когда создание требует нескольких шагов (например, валидация, настройка зависимостей).
- Когда важно иммутабельное состояние (Builder создаёт объект один раз и делает его неизменным).
🎯Ключевые преимущества:
- Читаемость кода — легко понять, что именно задаётся.
- Гибкость — можно создавать разные комбинации без взрыва конструкторов.
- Иммутабельность — готовый объект часто неизменяем.
- Инкапсуляция логики создания — в Builder можно встроить валидацию или вычисления.
- Расширяемость — легко добавить новый параметр, не ломая старый код.
⚠ Недостатки:
- Больше классов и кода — особенно если объект простой.
- Может быть избыточен для маленьких моделей с 1–2 параметрами.
- Необходимость дублировать поля (в классе и в Builder).
Пример. Вариант А:
/**
* Product
*/
public class Car {
private final String engine;
private final int doors;
private final String color;
private final boolean hybrid;
private Car(Builder builder) {
this.engine = builder.engine;
this.doors = builder.doors;
this.color = builder.color;
this.hybrid = builder.hybrid;
}
public static class Builder {
private String engine;
private int doors;
private String color;
private boolean hybrid;
public Builder engine(String engine) {
this.engine = engine;
return this;
}
public Builder doors(int doors) {
this.doors = doors;
return this;
}
public Builder color(String color) {
this.color = color;
return this;
}
public Builder hybrid(boolean hybrid) {
this.hybrid = hybrid;
return this;
}
public Car build() {
return new Car(this);
}
}
@Override
public String toString() {
return "Car{" +
"engine='" + engine + '\'' +
", doors=" + doors +
", color='" + color + '\'' +
", hybrid=" + hybrid +
'}';
}
}/**
* Demo
*/
public class Demo {
public static void main(String[] args) {
Car car = new Car.Builder()
.engine("V8")
.doors(2)
.color("black")
.hybrid(false)
.build();
System.out.println(car);
}
}Пример. Вариант B:
/**
* Product
*/
public class House {
private String foundation;
private String structure;
private String roof;
private int rooms;
private boolean garage;
private boolean garden;
private boolean pool;
// Пакетные сеттеры оставлены для билдера (в бою можно сделать package-private)
void setFoundation(String foundation) { this.foundation = foundation; }
void setStructure(String structure) { this.structure = structure; }
void setRoof(String roof) { this.roof = roof; }
void setRooms(int rooms) { this.rooms = rooms; }
void setGarage(boolean garage) { this.garage = garage; }
void setGarden(boolean garden) { this.garden = garden; }
void setPool(boolean pool) { this.pool = pool; }
@Override
public String toString() {
return "House{" +
"foundation='" + foundation + '\'' +
", structure='" + structure + '\'' +
", roof='" + roof + '\'' +
", rooms=" + rooms +
", garage=" + garage +
", garden=" + garden +
", pool=" + pool +
'}';
}
}/**
* Builder Interface
*/
public interface HouseBuilder {
void reset();
void buildFoundation();
void buildStructure();
void buildRoof();
void buildRooms(int count);
// опции
void addGarage();
void addGarden();
void addPool();
House getResult();
}/**
* Concrete Builder
*/
public class WoodenHouseBuilder implements HouseBuilder {
private House house;
public WoodenHouseBuilder() {
reset();
}
@Override
public void reset() {
house = new House();
}
@Override
public void buildFoundation() {
house.setFoundation("Wooden piles with concrete anchors");
}
@Override
public void buildStructure() {
house.setStructure("Timber frame (glued laminated wood)");
}
@Override
public void buildRoof() {
house.setRoof("Asphalt shingles on wooden rafters");
}
@Override
public void buildRooms(int count) {
if (count <= 0) throw new IllegalArgumentException("Rooms must be > 0");
house.setRooms(count);
}
@Override
public void addGarage() {
house.setGarage(true);
}
@Override
public void addGarden() {
house.setGarden(true);
}
@Override
public void addPool() {
house.setPool(true);
}
@Override
public House getResult() {
House result = house;
// защита от повторного использования без reset()
reset();
return result;
}
}/**
* Concrete Builder
*/
public class StoneHouseBuilder implements HouseBuilder {
private House house;
public StoneHouseBuilder() {
reset();
}
@Override
public void reset() {
house = new House();
}
@Override
public void buildFoundation() {
house.setFoundation("Monolithic concrete slab");
}
@Override
public void buildStructure() {
house.setStructure("Reinforced concrete + brick walls");
}
@Override
public void buildRoof() {
house.setRoof("Clay tiles on steel trusses");
}
@Override
public void buildRooms(int count) {
if (count <= 0) throw new IllegalArgumentException("Rooms must be > 0");
house.setRooms(count);
}
@Override
public void addGarage() {
house.setGarage(true);
}
@Override
public void addGarden() {
house.setGarden(true);
}
@Override
public void addPool() {
house.setPool(true);
}
@Override
public House getResult() {
House result = house;
reset();
return result;
}
}/**
* Director
*/
public class Director {
private HouseBuilder builder;
public Director(HouseBuilder builder) {
this.builder = builder;
}
public void setBuilder(HouseBuilder builder) {
this.builder = builder;
}
public House constructBasic() {
builder.reset();
builder.buildFoundation();
builder.buildStructure();
builder.buildRoof();
builder.buildRooms(2);
return builder.getResult();
}
public House constructFamily() {
builder.reset();
builder.buildFoundation();
builder.buildStructure();
builder.buildRoof();
builder.buildRooms(4);
builder.addGarden();
builder.addGarage();
return builder.getResult();
}
public House constructLuxury() {
builder.reset();
builder.buildFoundation();
builder.buildStructure();
builder.buildRoof();
builder.buildRooms(6);
builder.addGarage();
builder.addGarden();
builder.addPool();
return builder.getResult();
}
}/**
* Demo
*/
public class Demo {
public static void main(String[] args) {
// 1) Деревянные дома по трем рецептам
Director director = new Director(new WoodenHouseBuilder());
House woodBasic = director.constructBasic();
House woodFamily = director.constructFamily();
House woodLuxury = director.constructLuxury();
System.out.println("[WOOD] basic -> " + woodBasic);
System.out.println("[WOOD] family -> " + woodFamily);
System.out.println("[WOOD] luxury -> " + woodLuxury);
// 2) Переключаемся на каменный билдер — тот же сценарий, другие материалы
director.setBuilder(new StoneHouseBuilder());
House stoneBasic = director.constructBasic();
House stoneFamily = director.constructFamily();
House stoneLuxury = director.constructLuxury();
System.out.println("[STONE] basic -> " + stoneBasic);
System.out.println("[STONE] family -> " + stoneFamily);
System.out.println("[STONE] luxury -> " + stoneLuxury);
// 3) При желании — тонкая настройка «вручную» (без Director)
HouseBuilder custom = new StoneHouseBuilder();
custom.reset();
custom.buildFoundation();
custom.buildStructure();
custom.buildRoof();
custom.buildRooms(3);
custom.addGarage();
House tailorMade = custom.getResult();
System.out.println("[CUSTOM] stone(3r + garage) -> " + tailorMade);
}
}5) Prototype
🧭Назначение:
Позволить создавать новые объекты путём клонирования существующих экземпляров (прототипов), не привязываясь к их конкретным классам.
⚙Когда использовать:
- Когда создание объекта через
newслишком дорого (например, нужно загружать данные из БД или инициализировать ресурсы). - Когда нужно много похожих объектов с одинаковой базовой структурой.
- Когда система должна быть независима от конкретных классов создаваемых объектов (работа через интерфейсы
clone()).
🎯Ключевые преимущества:
- Производительность - Быстрее, чем создание через конструктор, если объект сложный.
- Изоляция от конкретных классов - Клиенту не нужно знать, что именно он копирует.
- Гибкость - Можно легко создавать вариации объектов — изменяя параметры после клонирования.
- Упрощает динамическое создание - Можно хранить «реестр прототипов» и клонировать нужный вариант на лету.
⚠ Недостатки:
- Сложность копирования - Если объект содержит вложенные объекты, нужно делать глубокое копирование (deep copy), иначе копии будут ссылаться на общие поля.
- Уязвимость при наследовании - Каждый подкласс должен корректно переопределять
clone()— легко ошибиться. - Неочевидность - Новички могут запутаться в поведении shallow vs deep clone.
- Трудность реализации при изменении структуры - Если добавляешь новые поля, надо не забыть добавить их и в конструктор-копию.
Пример:
/**
* Продукт (абстрактный прототип)
*/
public abstract class Shape implements Cloneable {
private String color;
public Shape(String color) {
this.color = color;
}
public Shape(Shape target) {
if (target != null) {
this.color = target.color;
}
}
public abstract Shape clone();
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}/**
* Конкретный прототип
*/
public class Circle extends Shape {
private int radius;
public Circle(String color, int radius) {
super(color);
this.radius = radius;
}
public Circle(Circle target) {
super(target);
if (target != null) {
this.radius = target.radius;
}
}
@Override
public Shape clone() {
return new Circle(this);
}
@Override
public String toString() {
return "Circle{" +
"color='" + getColor() + '\'' +
", radius=" + radius +
'}';
}
}/**
* Конкретный прототип
*/
public class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(String color, int width, int height) {
super(color);
this.width = width;
this.height = height;
}
public Rectangle(Rectangle target) {
super(target);
if (target != null) {
this.width = target.width;
this.height = target.height;
}
}
@Override
public Shape clone() {
return new Rectangle(this);
}
@Override
public String toString() {
return "Rectangle{" +
"color='" + getColor() + '\'' +
", width=" + width +
", height=" + height +
'}';
}
}/**
* Demo
*/
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
// Прототипы (образцы)
Circle prototypeCircle = new Circle("red", 10);
Rectangle prototypeRect = new Rectangle("blue", 20, 30);
// "Реестр прототипов" (часто используется вместе с паттерном Prototype)
Map<String, Shape> prototypes = new HashMap<>();
prototypes.put("smallCircle", prototypeCircle);
prototypes.put("rect", prototypeRect);
// Создаём копии
Circle clonedCircle = (Circle) prototypes.get("smallCircle").clone();
clonedCircle.setColor("green");
Rectangle clonedRect = (Rectangle) prototypes.get("rect").clone();
// Вывод
System.out.println("Original circle: " + prototypeCircle);
System.out.println("Cloned circle: " + clonedCircle);
System.out.println("Original rect: " + prototypeRect);
System.out.println("Cloned rect: " + clonedRect);
}
}Вывод:
Original circle: Circle{color='red', radius=10}
Cloned circle: Circle{color='green', radius=10}
Original rect: Rectangle{color='blue', width=20, height=30}
Cloned rect: Rectangle{color='blue', width=20, height=30}🧩Structural
1) Adapter
🧭Назначение:
Преобразовать интерфейс одного класса в интерфейс, ожидаемый другим классом.
Adapter делает несовместимые классы совместимыми, позволяя им взаимодействовать.
**⚙Когда использовать: **
- Когда нужно интегрировать старый (legacy) код в новую систему.
- Когда библиотека или внешний API имеют интерфейс, который нельзя изменить.
- Когда два класса решают похожие задачи, но имеют разные методы и сигнатуры.
- Когда нужно заменить интерфейс без переписывания существующего кода.
🎯Ключевые преимущества:
- Совместимость без изменения существующего кода
Можно использовать старые классы в новой архитектуре. - Инкапсуляция различий
Код клиента не знает, что работает через адаптер. - Повышение переиспользуемости
Позволяет интегрировать библиотеки, API или устройства с разными интерфейсами. - Чистота архитектуры
Логика адаптации сосредоточена в одном месте, а не размазана по коду.
⚠ Недостатки:
- Усложнение структуры
Добавляется дополнительный уровень абстракции (ещё один класс). - Потенциальная потеря производительности
При множественных уровнях адаптации (например, несколько вложенных адаптеров). - Иногда лечит симптомы, а не причину
Если интерфейсы слишком разные, адаптер превращается в “костыль”, а не в решение.
Пример:
/**
* Старый интерфейс (Target)
*/
public interface SDCard {
String readSD();
}/**
* Реализация старого интерфейса
*/
public class SDCardImpl implements SDCard {
@Override
public String readSD() {
return "Reading data from SD card...";
}
}/**
* Новый интерфейс (несовместимый)
*/
public interface USBDevice {
String readUSB();
}/**
* Реализация нового интерфейса
*/
public class USBReader implements USBDevice {
@Override
public String readUSB() {
return "Reading data from USB device...";
}
}/**
* Адаптер (Adapter)
*/
public class SDToUSBAdapter implements USBDevice {
private final SDCard sdCard;
public SDToUSBAdapter(SDCard sdCard) {
this.sdCard = sdCard;
}
@Override
public String readUSB() {
// адаптируем вызов SD-карты к интерфейсу USB
return sdCard.readSD();
}
}/**
* Клиентский код
*/
public class Demo {
public static void main(String[] args) {
// У нас есть SD-карта
SDCard sdCard = new SDCardImpl();
// Но устройство ожидает USB
USBDevice usbPort = new SDToUSBAdapter(sdCard);
// Работает, хотя SD подключена через адаптер!
System.out.println(usbPort.readUSB());
}
}Вывод:
Reading data from SD card...
🧩Варианты адаптеров:
- Object Adapter — через композицию (как в примере: адаптер содержит ссылку на адаптируемый объект).
- Class Adapter — через наследование (реализует один интерфейс и наследует другой класс).
Используется, если язык поддерживает множественное наследование (в Java — редко). - Bidirectional Adapter — адаптирует оба направления (встречается, например, при интеграции старого API с новым).
2) Facade
🧭Назначение:
Предоставить унифицированный, простой интерфейс к сложной подсистеме. Фасад определяет более высокий уровень интерфейса, который облегчает использование системы. Если упрощённо: вместо того, чтобы звонить в три разных отдела, ты звонишь в один call-центр (Facade), а он уже сам разбирается, кто и что должен сделать.
⚙Когда использовать:
- Когда система состоит из множества сложных классов или API, и нужно упростить взаимодействие с ней.
- Когда нужно разделить уровни системы (клиент не должен знать деталей реализации).
- Когда нужно снизить связанность между подсистемами.
- Когда надо создать удобную точку входа (entry point) в модуль.
🎯Ключевые преимущества:
- Упрощение взаимодействия
Клиенту не нужно знать внутреннюю структуру подсистемы. - Ослабление связанности
Клиент зависит только от фасада, а не от десятков конкретных классов. - Инкапсуляция сложности
Вся логика взаимодействия между компонентами скрыта внутри фасада. - Повышение читаемости и модульности
Фасады становятся логическими “вратами” в разные подсистемы. - Лёгкость рефакторинга
Можно изменять внутренние классы без влияния на код клиента.
⚠ Недостатки:
- Риск чрезмерной зависимости от фасада
Если клиенты начинают использовать только фасад, они теряют гибкость при необходимости работать с деталями. - Ограниченная функциональность
Иногда фасад не покрывает весь функционал подсистемы. - Скрывает проблемы архитектуры
Если подсистема слишком запутана, фасад может лишь “маскировать” её сложность, а не решать корень проблемы.
Пример:
/**
* Подсистема (внутренние классы)
*/
public class DVDPlayer {
public void on() { System.out.println("DVD Player ON"); }
public void play(String movie) { System.out.println("Playing movie: " + movie); }
public void off() { System.out.println("DVD Player OFF"); }
}
public class Projector {
public void on() { System.out.println("Projector ON"); }
public void wideScreenMode() { System.out.println("Projector in widescreen mode"); }
public void off() { System.out.println("Projector OFF"); }
}
public class Amplifier {
public void on() { System.out.println("Amplifier ON"); }
public void setVolume(int level) { System.out.println("Volume set to " + level); }
public void off() { System.out.println("Amplifier OFF"); }
}
public class Lights {
public void dim(int level) { System.out.println("Lights dimmed to " + level + "%"); }
public void on() { System.out.println("Lights ON"); }
}/**
* Фасад (упрощённый интерфейс)
*/
public class HomeTheaterFacade {
private final DVDPlayer dvd;
private final Projector projector;
private final Amplifier amp;
private final Lights lights;
public HomeTheaterFacade(DVDPlayer dvd, Projector projector, Amplifier amp, Lights lights) {
this.dvd = dvd;
this.projector = projector;
this.amp = amp;
this.lights = lights;
}
public void watchMovie(String movie) {
System.out.println("Getting ready to watch a movie...");
lights.dim(20);
projector.on();
projector.wideScreenMode();
amp.on();
amp.setVolume(8);
dvd.on();
dvd.play(movie);
}
public void endMovie() {
System.out.println("Shutting movie theater down...");
dvd.off();
amp.off();
projector.off();
lights.on();
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
DVDPlayer dvd = new DVDPlayer();
Projector projector = new Projector();
Amplifier amp = new Amplifier();
Lights lights = new Lights();
HomeTheaterFacade homeTheater = new HomeTheaterFacade(dvd, projector, amp, lights);
homeTheater.watchMovie("Inception");
System.out.println();
homeTheater.endMovie();
}
}Вывод:
Getting ready to watch a movie...
Lights dimmed to 20%
Projector ON
Projector in widescreen mode
Amplifier ON
Volume set to 8
DVD Player ON
Playing movie: Inception
Shutting movie theater down...
DVD Player OFF
Amplifier OFF
Projector OFF
Lights ON3) Proxy
🧭Назначение:
Позволяет подставить вместо реального объекта его «заместителя», который управляет доступом к нему. Прокси имеет тот же интерфейс, что и реальный объект, и перехватывает вызовы к нему.
Говоря просто: вместо того, чтобы напрямую говорить с «королём» (реальным объектом), ты общаешься с его «советником» (Proxy),
а он уже решает, стоит ли тревожить короля или можно ответить самому.
⚙Когда использовать:
- Когда нужно контролировать доступ к ресурсу (например, защита, аутентификация).
- Когда нужно отложить создание дорогого объекта (ленивая инициализация).
- Когда нужно выполнять удалённые вызовы (Remote Proxy).
- Когда нужно кешировать результаты (Caching Proxy).
- Когда нужно логировать, проверять, синхронизировать вызовы (Smart Proxy).
🎯Ключевые преимущества:
- Контроль доступа
Можно ограничить или фильтровать вызовы к объекту. - Ленивая инициализация
Создаём дорогие объекты (например, файлы, подключения, большие коллекции) только когда нужно. - Кеширование
Можно хранить результаты предыдущих вызовов и возвращать их повторно. - Безопасность и логирование
Можно внедрить аутентификацию, мониторинг, логи. - Сетевое взаимодействие
Прокси может представлять удалённый объект (Remote Proxy, например, RMI, REST).
⚠ Недостатки:
- Усложнение кода
Добавляется дополнительный слой между клиентом и объектом. - Сложность синхронизации
Если реальный объект меняет состояние, нужно аккуратно синхронизировать данные между Proxy и Real. - Потенциальные задержки
Если Proxy выполняет дополнительные проверки или сетевые вызовы, это может замедлить работу. - Скрытая логика
Из-за одинакового интерфейса может быть неочевидно, что объект — “заместитель”.
🧩Виды Proxy
| Вид | Назначение |
|---|---|
| Virtual Proxy | Отложенное создание «тяжёлых» объектов (пример выше). |
| Protection Proxy | Контроль прав доступа (например, проверка ролей пользователей). |
| Remote Proxy | Представляет объект, находящийся на другом сервере (например, RMI, gRPC, REST). |
| Smart Proxy | Добавляет функциональность при каждом обращении (логирование, подсчёт ссылок, транзакции). |
| Caching Proxy | Сохраняет результаты для повторного использования (например, при работе с API). |
Пример:
/**
* Интерфейс
*/
public interface Image {
void display();
}/**
* Реальный объект (который загружает картинку)
*/
public class RealImage implements Image {
private final String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading image from disk: " + filename);
try { Thread.sleep(1000); } catch (InterruptedException ignored) {} // имитация долгой загрузки
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}/**
* Прокси (заместитель)
*/
public class ProxyImage implements Image {
private RealImage realImage;
private final String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
// Ленивая инициализация — создаём RealImage только при первом вызове
if (realImage == null) {
realImage = new RealImage(filename);
} else {
System.out.println("Image already loaded — using cached instance");
}
realImage.display();
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
Image img = new ProxyImage("photo.jpg");
// первое обращение — загрузка с диска
img.display();
System.out.println();
// второе обращение — без загрузки
img.display();
}
}Вывод:
Loading image from disk: photo.jpg
Displaying image: photo.jpg
Image already loaded — using cached instance
Displaying image: photo.jpg
4) Decorator
🧭Назначение:
Динамически расширять поведение объекта, не изменяя его исходный класс. Позволяет гибко комбинировать функции через вложенные обёртки. Если метафорически: ты берёшь чашку кофе (базовый объект), добавляешь молоко — это один декоратор, сверху взбитые сливки — ещё один декоратор. И всё это всё ещё кофе, просто “с дополнениями”.
⚙Когда использовать:
- Когда нужно добавить функциональность объекту на лету, не меняя его класс.
- Когда нельзя использовать наследование (например, класс финальный или уже зафиксирован).
- Когда нужно гибко комбинировать разные поведения.
- Когда нужно заменить “жёсткие” иерархии наследования более лёгкой композицией.
🎯Ключевые преимущества:
- Динамическое добавление поведения
Можно “надевать” и “снимать” декораторы во время выполнения. - Избегание громоздкого наследования
Не нужно плодить подклассы вродеCoffeeWithMilkAndSugarAndWhip. - Гибкая комбинация функций
Любое количество декораторов можно комбинировать в любом порядке. - Следование принципу открытости/закрытости (OCP)
Код базовых классов не меняется при добавлении новой функциональности.
⚠ Недостатки:
- Усложнение структуры
Много мелких классов — для каждой “добавки” свой декоратор. - Трудность отладки
При глубокой вложенности трудно понять, какой слой сейчас выполняется. - Неудобство конфигурации
Нужно вручную оборачивать объекты (если не использовать DI или Builder).
Пример:
/**
* Компонент (базовый интерфейс)
*/
public interface Coffee {
String getDescription();
double getCost();
}/**
* Конкретный компонент (базовый напиток)
*/
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple coffee";
}
@Override
public double getCost() {
return 2.0;
}
}/**
* Абстрактный декоратор
*/
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
protected CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}/**
* Конкретный декоратор
*/
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", milk";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}/**
* Конкретный декоратор
*/
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", sugar";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}/**
* Конкретный декоратор
*/
public class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", whipped cream";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.7;
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " -> $" + coffee.getCost());
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription() + " -> $" + coffee.getCost());
}
}Вывод:
Simple coffee -> $2.0
Simple coffee, milk, sugar, whipped cream -> $3.4
5) Bridge
🧭Назначение:
Разделить абстракцию (интерфейс, который видит клиент) и её реализацию (внутренние детали), чтобы их можно было изменять независимо друг от друга. Если метафорически: абстракция — это пульт, реализация — это телевизор. Пульт может быть разный (универсальный, с голосом, с тачпадом), а телевизоры тоже разные (Samsung, LG, Sony) — и ты можешь использовать любой пульт с любым телевизором, если у них есть общий “мост”.
⚙Когда использовать:
- Когда нужно избежать взрыва подклассов, например:
CircleRed,CircleBlue,SquareRed,SquareBlue→ всё дублируется. С Bridge можно разделить фигуру (абстракция) и цвет (реализацию). - Когда абстракция и реализация должны развиваться независимо.
- Когда нужно переключаться между реализациями во время выполнения.
- Когда важно сократить связанность между слоями системы.
🎯Ключевые преимущества:
- Независимость абстракции и реализации
Можно добавлять новые фигуры и новые способы рисования независимо. - Гибкость
Можно подменить реализацию в рантайме (например, выбрать другой API). - Сокращение дублирования кода
Нет необходимости создавать комбинации классов для каждой пары “фигура + API”. - Следование принципу SRP и OCP
Каждый слой отвечает за своё, и новые функции не ломают старые.
⚠ Недостатки:
- Усложнение структуры
Нужно больше классов и интерфейсов, чем при простом наследовании. - Повышенная абстракция
Для новичков код может выглядеть избыточно, особенно без опыта. - Излишне для маленьких систем
Если нет необходимости отделять реализацию — проще обойтись без Bridge.
Пример:
/**
* Реализация (Implementor)
*/
public interface DrawingAPI {
void drawCircle(double x, double y, double radius);
}/**
* Конкретные реализации
*/
public class OpenGLAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.println("Drawing circle with OpenGL at (" + x + ", " + y + ") radius " + radius);
}
}
public class DirectXAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.println("Drawing circle with DirectX at (" + x + ", " + y + ") radius " + radius);
}
}/**
* Абстракция
*/
public abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
public abstract void resize(double factor);
}/**
* Конкретная абстракция
*/
public class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
drawingAPI.drawCircle(x, y, radius);
}
@Override
public void resize(double factor) {
radius *= factor;
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
Shape circleOpenGL = new Circle(10, 20, 5, new OpenGLAPI());
Shape circleDirectX = new Circle(15, 25, 7, new DirectXAPI());
circleOpenGL.draw();
circleDirectX.draw();
circleOpenGL.resize(2);
circleOpenGL.draw();
}
}
Вывод:
Drawing circle with OpenGL at (10.0, 20.0) radius 5.0
Drawing circle with DirectX at (15.0, 25.0) radius 7.0
Drawing circle with OpenGL at (10.0, 20.0) radius 10.0
6) Composite
🧭Назначение:
Объединить объекты в структуру «дерево» и позволить обращаться к отдельным объектам и их составам единообразно.
Если метафорически: представь папки и файлы. Файл — это “лист”, папка — “композит”, который может содержать другие файлы и папки. Но и у файлов, и у папок есть общий интерфейс — например, getSize().
⚙Когда использовать:
- Когда нужно представить иерархические структуры (деревья, меню, организационные схемы).
- Когда клиенту не важно, работает он с одним объектом или группой объектов.
- Когда нужно упростить код, чтобы не проверять тип объекта перед каждым действием.
🎯Ключевые преимущества:
- Единый интерфейс для всех уровней
Клиент не заботится о том, с чем работает — с “листом” или “ветвью”. - Рекурсивные структуры
Можно строить деревья любой глубины. - Простота добавления новых элементов
Новые типы листов и узлов легко добавляются, если они реализуют общий интерфейс. - Чистый и расширяемый код
Нетif (obj instanceof Folder)— просто вызываешь метод.
⚠ Недостатки:
- Сложность управления родителями и детьми
Если нужно знать, кто чей “родитель”, появляется дополнительная логика. - Сложнее ограничивать доступ
Трудно запретить добавление определённых типов объектов, так как интерфейс общий. - Потенциальное дублирование поведения
Некоторые методы могут быть неуместны для листов (например,add()в файле).
Пример:
/**
* Компонент (общий интерфейс)
*/
public interface FileSystemComponent {
void showDetails();
int getSize();
}/**
* Лист (файл)
*/
public class FileLeaf implements FileSystemComponent {
private final String name;
private final int size; // размер в КБ
public FileLeaf(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public void showDetails() {
System.out.println("File: " + name + " (" + size + "KB)");
}
@Override
public int getSize() {
return size;
}
}/**
* Композит (папка)
*/
import java.util.ArrayList;
import java.util.List;
public class DirectoryComposite implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> components = new ArrayList<>();
public DirectoryComposite(String name) {
this.name = name;
}
public void add(FileSystemComponent component) {
components.add(component);
}
public void remove(FileSystemComponent component) {
components.remove(component);
}
@Override
public void showDetails() {
System.out.println("Directory: " + name);
for (FileSystemComponent component : components) {
component.showDetails();
}
}
@Override
public int getSize() {
int total = 0;
for (FileSystemComponent component : components) {
total += component.getSize();
}
return total;
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
// файлы
FileSystemComponent file1 = new FileLeaf("notes.txt", 12);
FileSystemComponent file2 = new FileLeaf("photo.jpg", 340);
FileSystemComponent file3 = new FileLeaf("video.mp4", 2048);
// папка "Документы"
DirectoryComposite docs = new DirectoryComposite("Documents");
docs.add(file1);
docs.add(file2);
// папка "Видео"
DirectoryComposite videos = new DirectoryComposite("Videos");
videos.add(file3);
// главная папка
DirectoryComposite root = new DirectoryComposite("Root");
root.add(docs);
root.add(videos);
// просмотр
root.showDetails();
System.out.println("\nTotal size: " + root.getSize() + "KB");
}
}Вывод:
Directory: Root
Directory: Documents
File: notes.txt (12KB)
File: photo.jpg (340KB)
Directory: Videos
File: video.mp4 (2048KB)
Total size: 2400KB
🧱Behavioral
1) Observer
🧭Назначение:
Определить зависимость «один-ко-многим» между объектами так, чтобы при изменении состояния одного все зависимые объекты автоматически уведомлялись и обновлялись. Если метафорически: это как YouTube-канал и подписчики — канал выпускает новое видео (событие), и все подписчики получают уведомление, не спрашивая каждый раз “а вышло ли что-то новое?”.
⚙Когда использовать:
- Когда одно изменение должно автоматически оповестить несколько других объектов.
- Когда между объектами есть слабая связь, и издатель не должен знать, кто на него подписан.
- Когда нужно синхронизировать состояния нескольких компонентов (например, UI и модель данных).
- Когда нужно реализовать реактивное поведение (event-driven).
🎯Ключевые преимущества:
- Слабая связанность
Издатель не знает, кто именно подписан — можно добавлять, удалять или менять наблюдателей без изменения кода издателя. - Автоматическая синхронизация
Все подписчики получают актуальные данные без ручного опроса. - Расширяемость
Можно легко добавить новых наблюдателей с разным поведением. - Поддерживает принцип OCP — можно добавлять новых слушателей без изменения логики субъекта.
⚠ Недостатки:
- Трудно отладить
Множество скрытых связей — сложно понять, кто кого уведомляет. - Последовательность уведомлений
Может быть непредсказуемой, если наблюдателей много. - Проблемы с производительностью
При большом числе подписчиков уведомления становятся дорогими. - Возможность “утечек” наблюдателей
Если не удалять слушателей, ссылки могут удерживать объекты в памяти.
Пример:
/**
* Интерфейс наблюдателя
*/
public interface Observer {
void update(float temperature, float humidity);
}/**
* Интерфейс издателя (Subject)
*/
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}/**
* Конкретный издатель
*/
import java.util.ArrayList;
import java.util.List;
public class WeatherStation implements Subject {
private final List<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity);
}
}
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers(); // уведомляем всех при изменении
}
}/**
* Конкретные наблюдатели
*/
public class PhoneDisplay implements Observer {
@Override
public void update(float temperature, float humidity) {
System.out.println("📱 Phone Display -> Temp: " + temperature + "°C, Humidity: " + humidity + "%");
}
}
public class WebDisplay implements Observer {
@Override
public void update(float temperature, float humidity) {
System.out.println("🌐 Web Dashboard -> Temp: " + temperature + "°C, Humidity: " + humidity + "%");
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
Observer phone = new PhoneDisplay();
Observer web = new WebDisplay();
station.registerObserver(phone);
station.registerObserver(web);
station.setMeasurements(22.5f, 65f);
station.setMeasurements(25.0f, 70f);
}
}Вывод:
📱 Phone Display -> Temp: 22.5°C, Humidity: 65.0%
🌐 Web Dashboard -> Temp: 22.5°C, Humidity: 65.0%
📱 Phone Display -> Temp: 25.0°C, Humidity: 70.0%
🌐 Web Dashboard -> Temp: 25.0°C, Humidity: 70.0%
2) Command
🧭Назначение:
Инкапсулировать запрос (действие) как объект, чтобы можно было параметризовать клиентов разными запросами, ставить запросы в очередь, отменять и повторять их выполнение. Если метафорически: официант (Invoker) принимает заказ (Command) и передаёт его на кухню (Receiver). Официант не готовит сам — он просто знает, что нужно сделать и кому передать.
⚙Когда использовать:
- Когда нужно отделить отправителя команды от получателя, чтобы они не знали друг о друге напрямую.
- Когда требуется undo / redo (отмена и повтор действий).
- Когда нужно составлять очереди операций или макрокоманды (серии действий).
- Когда действия нужно логировать и воспроизводить.
- Когда нужно централизованно управлять командами (например, кнопками в UI).
🎯Ключевые преимущества:
- Отделяет отправителя от получателя
Объекты не зависят друг от друга напрямую. - Позволяет отменять и повторять действия
Каждая команда знает, как себя отменить. - Поддерживает очереди и макрокоманды
Можно выполнять несколько команд подряд. - Простое логирование и аудирование
Команды можно сохранять для анализа или воспроизведения. - Расширяемость
Новые команды легко добавить, не меняя существующий код (OCP).
⚠ Недостатки:
- Много мелких классов
Для каждой операции — отдельный класс. - Усложнение архитектуры
Простые действия становятся избыточно обёрнутыми. - Дополнительные затраты памяти и времени
Если команд много, особенно при хранении для undo/redo.
Пример:
/**
* Интерфейс команды
*/
public interface Command {
void execute();
void undo();
}/**
* Получатели (Receivers)
*/
public class Light {
private final String location;
public Light(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " light is ON");
}
public void off() {
System.out.println(location + " light is OFF");
}
}
public class Stereo {
private final String location;
public Stereo(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " stereo is ON");
}
public void off() {
System.out.println(location + " stereo is OFF");
}
}/**
* Конкретные команды
*/
public class LightOnCommand implements Command {
private final Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
private final Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
public class StereoOnCommand implements Command {
private final Stereo stereo;
public StereoOnCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.on();
}
@Override
public void undo() {
stereo.off();
}
}/**
* Инициатор (Invoker)
*/
public class RemoteControl {
private Command lastCommand;
public void submit(Command command) {
command.execute();
lastCommand = command;
}
public void undo() {
if (lastCommand != null) {
System.out.println("Undoing last command...");
lastCommand.undo();
}
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
Light livingRoomLight = new Light("Living room");
Stereo stereo = new Stereo("Living room");
Command lightOn = new LightOnCommand(livingRoomLight);
Command lightOff = new LightOffCommand(livingRoomLight);
Command stereoOn = new StereoOnCommand(stereo);
RemoteControl remote = new RemoteControl();
remote.submit(lightOn);
remote.submit(stereoOn);
remote.submit(lightOff);
remote.undo(); // отмена последней команды
}
}Вывод:
Living room light is ON
Living room stereo is ON
Living room light is OFF
Undoing last command...
Living room light is ON
3) Strategy
🧭Назначение:
Определить семейство алгоритмов, инкапсулировать каждый из них и сделать взаимозаменяемыми. Это позволяет изменять алгоритм независимо от клиента, который его использует.
Если метафорически: у тебя есть навигация, и ты можешь выбрать —
ехать на машине, на велосипеде или пешком. Алгоритмы разные, но интерфейс один — «построить маршрут».
⚙Когда использовать:
- Когда нужно менять поведение объекта во время выполнения.
- Когда в классе есть много
if/switchс разными вариантами алгоритмов. - Когда алгоритмы часто изменяются или расширяются.
- Когда нужно разделить логику (что делает клиент) и варианты выполнения (как именно делать).
🎯Ключевые преимущества:
- Гибкость и расширяемость
Добавление нового алгоритма не требует изменения клиента. - Избавление от условных операторов
Вместоif/else— просто выбираем стратегию. - Следует принципам SOLID
- OCP — добавляем новые стратегии без изменения старого кода.
- SRP — каждая стратегия выполняет только один алгоритм.
- Поддержка runtime-переключения
Можно менять поведение программы прямо во время работы.
⚠ Недостатки:
- Увеличение числа классов
Для каждой стратегии создаётся отдельный класс. - Не всегда оправдан
Для простых случаевif-блок может быть проще. - Клиент должен знать о стратегиях
Нужно понимать, какую стратегию выбрать и когда.
Пример:
/**
* Интерфейс стратегии
*/
public interface PaymentStrategy {
void pay(int amount);
}/**
* Конкретные стратегии
*/
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + "€ using Credit Card (" + cardNumber + ")");
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + "€ using PayPal (" + email + ")");
}
}
public class CashPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + "€ in cash");
}
}/**
* Контекст (клиент, использующий стратегию)
*/
public class Order {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processOrder(int amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("No payment strategy set!");
}
paymentStrategy.pay(amount);
}
}/**
* Использование
*/
public class Demo {
public static void main(String[] args) {
Order order = new Order();
// клиент выбирает стратегию оплаты
order.setPaymentStrategy(new CreditCardPayment("1234-5678-9012"));
order.processOrder(50);
order.setPaymentStrategy(new PayPalPayment("user@example.com"));
order.processOrder(75);
order.setPaymentStrategy(new CashPayment());
order.processOrder(20);
}
}Вывод:
Paid 50€ using Credit Card (1234-5678-9012)
Paid 75€ using PayPal (user@example.com)
Paid 20€ in cash
4) Iterator
🧭Назначение:
Предоставить единый интерфейс для последовательного доступа к элементам коллекции, не раскрывая её внутреннего представления (список, дерево, граф и т. д.). Если метафорически: это гид по музею, который знает, как вести тебя от одной картины к другой, не заставляя тебя разбираться в планировке здания.
⚙Когда использовать:
- Когда нужно обходить сложные структуры данных (списки, деревья, графы).
- Когда нужно инкапсулировать логику обхода, чтобы клиент не знал, как именно хранятся элементы.
- Когда нужно одновременно поддерживать несколько способов обхода (вперёд, назад, по уровням).
- Когда важно сделать коллекцию универсальной для разных способов итерации.
🎯Ключевые преимущества:
- Инкапсуляция структуры коллекции
Клиент не знает, как данные хранятся (массив, список, дерево). - Универсальный интерфейс обхода
Можно одинаково работать с любыми коллекциями. - Несколько итераторов одновременно
Можно иметь несколько активных обходов одной коллекции. - Разные типы обхода
Например, прямой, обратный, по уровням и т. д. - Поддержка принципов SRP и OCP
Коллекция отвечает за хранение, итератор — за обход.
⚠ Недостатки:
- Дополнительные классы
Каждый тип итерации требует отдельного итератора. - Не всегда эффективен
Для больших структур возможны издержки на создание итераторов. - Потенциальная несогласованность
Если коллекция изменяется во время итерации — может привести к ошибкам (ConcurrentModificationException в Java).
Пример:
/**
* Интерфейс итератора
*/
public interface Iterator<T> {
boolean hasNext();
T next();
}/**
* Интерфейс коллекции
*/
public interface Container<T> {
Iterator<T> getIterator();
}/**
* Конкретная коллекция
*/
public class NameRepository implements Container<String> {
private final String[] names = {"Alice", "Bob", "Charlie", "Diana"};
@Override
public Iterator<String> getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator<String> {
int index = 0;
@Override
public boolean hasNext() {
return index < names.length;
}
@Override
public String next() {
if (!hasNext()) {
throw new IllegalStateException("No more elements");
}
return names[index++];
}
}
}/**
* Клиент
*/
public class Demo {
public static void main(String[] args) {
NameRepository repository = new NameRepository();
Iterator<String> iterator = repository.getIterator();
while (iterator.hasNext()) {
System.out.println("Name: " + iterator.next());
}
}
}Вывод:
Name: Alice
Name: Bob
Name: Charlie
Name: Diana
5) Visitor
🧭Назначение:
Разделить алгоритмы и структуру данных, позволяя добавлять новые операции, не изменяя классы элементов.
Метафора: ты — посетитель музея. У музея есть разные экспонаты (картины, скульптуры, артефакты). Каждый экспонат принимает тебя (accept(visitor)), и ты решаешь, что делать с каждым типом экспоната — сфотографировать, оценить, описать и т. д.
⚙Когда использовать:
- Когда нужно выполнять много разных операций над объектами сложной структуры (например, дерево, AST, файловая система).
- Когда классы объектов редко меняются, но операции над ними — часто.
- Когда нужно добавлять поведение без изменения классов (нарушая принцип открытости/закрытости в классическом смысле).
- Когда нужно отделить поведение от структуры данных.
🎯Ключевые преимущества:
- Добавление новых операций без изменения классов элементов
Просто создаёшь новогоVisitor. - Разделение логики и структуры данных
Элементы описывают что они такое, а посетитель — что с ними делать. - Одинаковая обработка разных типов элементов
Один и тот же посетитель может работать со многими типами. - Чистая организация кода
Все алгоритмы собраны в одном месте, а не разбросаны по классам.
⚠ Недостатки:
- Сложность при добавлении новых типов элементов
Придётся менять всех посетителей (нарушение OCP для Visitor). - Зависимость от структуры
Посетитель должен знать все типы элементов. - Много кода и дублирования
При большом числе типов и операций количество методов растёт. - Неподходит для часто меняющихся структур
Если классы элементов меняются — Visitor придётся переписывать.
Пример:
/**
* Интерфейс элемента (принимает посетителя)
*/
public interface DocumentElement {
void accept(Visitor visitor);
}/**
* Конкретные элементы
*/
public class Paragraph implements DocumentElement {
private final String text;
public Paragraph(String text) {
this.text = text;
}
public String getText() {
return text;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Image implements DocumentElement {
private final String fileName;
public Image(String fileName) {
this.fileName = fileName;
}
public String getFileName() {
return fileName;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}/**
* Интерфейс посетителя
*/
public interface Visitor {
void visit(Paragraph paragraph);
void visit(Image image);
}/**
* Конкретные посетители
*/
public class RenderVisitor implements Visitor {
@Override
public void visit(Paragraph paragraph) {
System.out.println("📝 Rendering text: " + paragraph.getText());
}
@Override
public void visit(Image image) {
System.out.println("🖼 Rendering image: " + image.getFileName());
}
}
public class ExportVisitor implements Visitor {
@Override
public void visit(Paragraph paragraph) {
System.out.println("📤 Exporting paragraph to PDF: " + paragraph.getText());
}
@Override
public void visit(Image image) {
System.out.println("📤 Exporting image to ZIP: " + image.getFileName());
}
}/**
* Клиент
*/
import java.util.List;
public class Demo {
public static void main(String[] args) {
List<DocumentElement> document = List.of(
new Paragraph("Hello, Visitor pattern!"),
new Image("diagram.png"),
new Paragraph("End of document.")
);
Visitor renderer = new RenderVisitor();
Visitor exporter = new ExportVisitor();
System.out.println("--- Rendering document ---");
document.forEach(e -> e.accept(renderer));
System.out.println("\n--- Exporting document ---");
document.forEach(e -> e.accept(exporter));
}
}Вывод:
--- Rendering document ---
📝 Rendering text: Hello, Visitor pattern!
🖼 Rendering image: diagram.png
📝 Rendering text: End of document.
--- Exporting document ---
📤 Exporting paragraph to PDF: Hello, Visitor pattern!
📤 Exporting image to ZIP: diagram.png
📤 Exporting paragraph to PDF: End of document.
















