RU | EN | DE

🧱Creational

1) Singleton

singleton.jpeg

🧭Назначение:

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

⚙Когда использовать:

– классы для работы с конфигурацией;
– классы логирования;
– пул соединений с БД.

🎯Ключевые преимущества:

  • Гарантия единственного экземпляра
    Класс сам следит, чтобы был создан только один объект, независимо от числа вызовов 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

factory.jpeg

🧭Назначение:

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

⚙Когда использовать:

– 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

abstractfactory.jpg

🧭Назначение:

Она предоставляет интерфейс для создания объектов, где каждый конкретный подкласс решает, какие именно конкретные реализации использовать.

⚙Когда использовать:

  • 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

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

builder.jpg

🧭Назначение:

Отделить процесс конструирования сложного объекта от его представления,
чтобы один и тот же процесс можно было использовать для создания разных представлений. Говоря проще — чтобы не мучиться с длинными конструкторами вроде
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

prototype.jpg

prototype_2.jpg

🧭Назначение:

Позволить создавать новые объекты путём клонирования существующих экземпляров (прототипов), не привязываясь к их конкретным классам.

⚙Когда использовать:

  • Когда создание объекта через 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.jpg

🧭Назначение:

Преобразовать интерфейс одного класса в интерфейс, ожидаемый другим классом.
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...

🧩Варианты адаптеров:

  1. Object Adapter — через композицию (как в примере: адаптер содержит ссылку на адаптируемый объект).
  2. Class Adapter — через наследование (реализует один интерфейс и наследует другой класс).
    Используется, если язык поддерживает множественное наследование (в Java — редко).
  3. Bidirectional Adapter — адаптирует оба направления (встречается, например, при интеграции старого API с новым).

2) Facade

facade.jpg

🧭Назначение:

Предоставить унифицированный, простой интерфейс к сложной подсистеме. Фасад определяет более высокий уровень интерфейса, который облегчает использование системы. Если упрощённо: вместо того, чтобы звонить в три разных отдела, ты звонишь в один 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 ON

3) Proxy

proxy.jpg

🧭Назначение:

Позволяет подставить вместо реального объекта его «заместителя», который управляет доступом к нему. Прокси имеет тот же интерфейс, что и реальный объект, и перехватывает вызовы к нему. Говоря просто: вместо того, чтобы напрямую говорить с «королём» (реальным объектом), ты общаешься с его «советником» (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

decorator.jpg

🧭Назначение:

Динамически расширять поведение объекта, не изменяя его исходный класс. Позволяет гибко комбинировать функции через вложенные обёртки. Если метафорически: ты берёшь чашку кофе (базовый объект), добавляешь молоко — это один декоратор, сверху взбитые сливки — ещё один декоратор. И всё это всё ещё кофе, просто “с дополнениями”.

⚙Когда использовать:

  • Когда нужно добавить функциональность объекту на лету, не меняя его класс.
  • Когда нельзя использовать наследование (например, класс финальный или уже зафиксирован).
  • Когда нужно гибко комбинировать разные поведения.
  • Когда нужно заменить “жёсткие” иерархии наследования более лёгкой композицией.

🎯Ключевые преимущества:

  • Динамическое добавление поведения
    Можно “надевать” и “снимать” декораторы во время выполнения.
  • Избегание громоздкого наследования
    Не нужно плодить подклассы вроде 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

bridge.jpg

🧭Назначение:

Разделить абстракцию (интерфейс, который видит клиент) и её реализацию (внутренние детали), чтобы их можно было изменять независимо друг от друга. Если метафорически: абстракция — это пульт, реализация — это телевизор. Пульт может быть разный (универсальный, с голосом, с тачпадом), а телевизоры тоже разные (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

composite.jpg

🧭Назначение:

Объединить объекты в структуру «дерево» и позволить обращаться к отдельным объектам и их составам единообразно. Если метафорически: представь папки и файлы. Файл — это “лист”, папка — “композит”, который может содержать другие файлы и папки. Но и у файлов, и у папок есть общий интерфейс — например, 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

observer.jpg

🧭Назначение:

Определить зависимость «один-ко-многим» между объектами так, чтобы при изменении состояния одного все зависимые объекты автоматически уведомлялись и обновлялись. Если метафорически: это как 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

command.jpg

🧭Назначение:

Инкапсулировать запрос (действие) как объект, чтобы можно было параметризовать клиентов разными запросами, ставить запросы в очередь, отменять и повторять их выполнение. Если метафорически: официант (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

strategy.jpg

🧭Назначение:

Определить семейство алгоритмов, инкапсулировать каждый из них и сделать взаимозаменяемыми. Это позволяет изменять алгоритм независимо от клиента, который его использует. Если метафорически: у тебя есть навигация, и ты можешь выбрать —
ехать на машине, на велосипеде или пешком. Алгоритмы разные, но интерфейс один — «построить маршрут».

⚙Когда использовать:

  • Когда нужно менять поведение объекта во время выполнения.
  • Когда в классе есть много 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

iterator.jpg

🧭Назначение:

Предоставить единый интерфейс для последовательного доступа к элементам коллекции, не раскрывая её внутреннего представления (список, дерево, граф и т. д.). Если метафорически: это гид по музею, который знает, как вести тебя от одной картины к другой, не заставляя тебя разбираться в планировке здания.

⚙Когда использовать:

  • Когда нужно обходить сложные структуры данных (списки, деревья, графы).
  • Когда нужно инкапсулировать логику обхода, чтобы клиент не знал, как именно хранятся элементы.
  • Когда нужно одновременно поддерживать несколько способов обхода (вперёд, назад, по уровням).
  • Когда важно сделать коллекцию универсальной для разных способов итерации.

🎯Ключевые преимущества:

  • Инкапсуляция структуры коллекции
    Клиент не знает, как данные хранятся (массив, список, дерево).
  • Универсальный интерфейс обхода
    Можно одинаково работать с любыми коллекциями.
  • Несколько итераторов одновременно
    Можно иметь несколько активных обходов одной коллекции.
  • Разные типы обхода
    Например, прямой, обратный, по уровням и т. д.
  • Поддержка принципов 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

visitor.jpg

🧭Назначение:

Разделить алгоритмы и структуру данных, позволяя добавлять новые операции, не изменяя классы элементов. Метафора: ты — посетитель музея. У музея есть разные экспонаты (картины, скульптуры, артефакты). Каждый экспонат принимает тебя (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.