Паттерны проектирования в Java: продвинутые примеры и реализация

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Продвинутые паттерны проектирования в Java: от теории к сложным примерам

Паттерны проектирования — это проверенные решения типовых задач, возникающих при разработке программного обеспечения. Если базовые паттерны (Singleton, Factory, Observer) знакомы большинству Java-разработчиков, то продвинутые паттерны требуют глубокого понимания архитектуры и умения применять их в сложных бизнес-сценариях. В этой статье мы разберем три мощных паттерна: Visitor, Command и State — с реальными примерами на Java, которые помогут вам писать более гибкий и поддерживаемый код.

1. Паттерн Visitor: отделение алгоритмов от структуры данных

Назначение: Позволяет добавлять новые операции для объектов различных классов, не изменяя сами классы. Идеально подходит для работы со сложными иерархиями, где требуется выполнять разные действия в зависимости от типа объекта.

Проблема: Представьте систему документооборота, где есть классы TextDocument, ImageDocument и SpreadsheetDocument. Вам нужно реализовать экспорт в PDF, HTML и TXT. Без Visitor каждый новый формат экспорта потребует изменения всех классов документов.

Решение с Visitor:

// 1. Интерфейс элемента (документа)
interface DocumentElement {
    void accept(DocumentVisitor visitor);
}

// 2. Конкретные элементы
class TextDocument implements DocumentElement {
    private String content;
    
    public TextDocument(String content) {
        this.content = content;
    }
    
    public String getContent() { return content; }
    
    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

class ImageDocument implements DocumentElement {
    private byte[] imageData;
    
    public ImageDocument(byte[] data) {
        this.imageData = data;
    }
    
    public byte[] getImageData() { return imageData; }
    
    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

// 3. Интерфейс посетителя
interface DocumentVisitor {
    void visit(TextDocument doc);
    void visit(ImageDocument doc);
    void visit(SpreadsheetDocument doc);
}

// 4. Конкретный посетитель — экспорт в HTML
class HtmlExporter implements DocumentVisitor {
    private StringBuilder sb = new StringBuilder();
    
    @Override
    public void visit(TextDocument doc) {
        sb.append("<html><body><p>")
          .append(doc.getContent())
          .append("</p></body></html>");
    }
    
    @Override
    public void visit(ImageDocument doc) {
        sb.append("<html><body><img src='data:image/png;base64,")
          .append(Base64.getEncoder().encodeToString(doc.getImageData()))
          .append("'/></body></html>");
    }
    
    @Override
    public void visit(SpreadsheetDocument doc) {
        sb.append("<html><body><table>...</table></body></html>");
    }
    
    public String getResult() { return sb.toString(); }
}

// 5. Клиентский код
public class DocumentSystem {
    public static void main(String[] args) {
        List<DocumentElement> documents = Arrays.asList(
            new TextDocument("Hello World"),
            new ImageDocument(new byte[]{1,2,3})
        );
        
        HtmlExporter exporter = new HtmlExporter();
        for (DocumentElement doc : documents) {
            doc.accept(exporter);
        }
        System.out.println(exporter.getResult());
    }
}

Ключевое преимущество: Для добавления нового формата (например, JSON) достаточно создать новый класс JsonExporter implements DocumentVisitor — ни один из классов документов не меняется.

2. Паттерн Command: инкапсуляция запросов как объектов

Назначение: Превращает запрос в объект, что позволяет параметризовать клиенты очередями запросов, логировать их, отменять операции и поддерживать транзакции.

Проблема: В текстовом редакторе операции (копирование, вставка, удаление) должны поддерживать отмену (undo) и повтор (redo). Без Command каждый метод будет содержать дублирующийся код для управления историей.

Решение с Command:

// 1. Интерфейс команды
interface Command {
    void execute();
    void undo();
}

// 2. Получатель (Receiver) — текстовый буфер
class TextBuffer {
    private StringBuilder content = new StringBuilder();
    private int cursorPosition = 0;
    
    public void insert(String text) {
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
    }
    
    public void delete(int length) {
        if (cursorPosition >= length) {
            content.delete(cursorPosition - length, cursorPosition);
            cursorPosition -= length;
        }
    }
    
    public String getContent() { return content.toString(); }
    public int getCursorPosition() { return cursorPosition; }
}

// 3. Конкретные команды
class InsertCommand implements Command {
    private TextBuffer buffer;
    private String text;
    private int savedPosition;
    
    public InsertCommand(TextBuffer buffer, String text) {
        this.buffer = buffer;
        this.text = text;
    }
    
    @Override
    public void execute() {
        savedPosition = buffer.getCursorPosition();
        buffer.insert(text);
    }
    
    @Override
    public void undo() {
        buffer.delete(text.length());
    }
}

class DeleteCommand implements Command {
    private TextBuffer buffer;
    private int length;
    private String deletedText;
    
    public DeleteCommand(TextBuffer buffer, int length) {
        this.buffer = buffer;
        this.length = length;
    }
    
    @Override
    public void execute() {
        // Сохраняем удаляемый текст для отмены
        int pos = buffer.getCursorPosition();
        deletedText = buffer.getContent().substring(pos - length, pos);
        buffer.delete(length);
    }
    
    @Override
    public void undo() {
        buffer.insert(deletedText);
    }
}

// 4. Invoker — диспетчер команд с поддержкой undo/redo
class CommandManager {
    private Stack<Command> history = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();
    
    public void executeCommand(Command cmd) {
        cmd.execute();
        history.push(cmd);
        redoStack.clear(); // новый запрос сбрасывает redo
    }
    
    public void undo() {
        if (!history.isEmpty()) {
            Command cmd = history.pop();
            cmd.undo();
            redoStack.push(cmd);
        }
    }
    
    public void redo() {
        if (!redoStack.isEmpty()) {
            Command cmd = redoStack.pop();
            cmd.execute();
            history.push(cmd);
        }
    }
}

// 5. Клиентский код
public class TextEditor {
    public static void main(String[] args) {
        TextBuffer buffer = new TextBuffer();
        CommandManager manager = new CommandManager();
        
        manager.executeCommand(new InsertCommand(buffer, "Привет, "));
        manager.executeCommand(new InsertCommand(buffer, "мир!"));
        manager.executeCommand(new DeleteCommand(buffer, 4)); // удаляем "мир!"
        
        System.out.println(buffer.getContent());                

Блоги

Рекомендаций по книгам