Продвинутые паттерны проектирования в 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());