Продвинутые паттерны проектирования в Java: от Bridge до Visitor
Паттерны проектирования — это проверенные решения типовых задач архитектуры программного обеспечения. Если базовые паттерны (Singleton, Factory, Observer) знакомы каждому Java-разработчику, то продвинутые паттерны часто остаются за кадром. В этой статье мы разберём три мощных паттерна: Bridge, Visitor и Memento. Вы узнаете, когда их применять, и получите готовые примеры кода на Java.
1. Bridge (Мост) — отделяем абстракцию от реализации
Паттерн Bridge используется, когда нужно разделить абстракцию и её реализацию, чтобы они могли изменяться независимо. Это особенно полезно, когда у вас есть несколько измерений изменений (например, разные типы устройств и разные пульты управления).
Когда применять: когда вы хотите избежать «взрыва классов» из-за комбинации разных вариантов.
Пример: Управление устройствами
Представьте, что у нас есть пульты (Remote) и устройства (Device). Мы хотим, чтобы один пульт мог работать с разными устройствами (TV, Radio).
// Реализация (Device)
interface Device {
void turnOn();
void turnOff();
void setVolume(int percent);
}
class TV implements Device {
@Override
public void turnOn() { System.out.println("TV включён"); }
@Override
public void turnOff() { System.out.println("TV выключен"); }
@Override
public void setVolume(int percent) { System.out.println("Громкость TV: " + percent + "%"); }
}
class Radio implements Device {
@Override
public void turnOn() { System.out.println("Radio включено"); }
@Override
public void turnOff() { System.out.println("Radio выключено"); }
@Override
public void setVolume(int percent) { System.out.println("Громкость Radio: " + percent + "%"); }
}
// Абстракция (Remote)
abstract class Remote {
protected Device device;
public Remote(Device device) {
this.device = device;
}
public abstract void togglePower();
public abstract void volumeUp();
public abstract void volumeDown();
}
class BasicRemote extends Remote {
private boolean isOn = false;
private int volume = 10;
public BasicRemote(Device device) {
super(device);
}
@Override
public void togglePower() {
if (isOn) { device.turnOff(); isOn = false; }
else { device.turnOn(); isOn = true; }
}
@Override
public void volumeUp() {
volume += 5;
device.setVolume(volume);
}
@Override
public void volumeDown() {
volume -= 5;
device.setVolume(volume);
}
}
// Использование
public class BridgeDemo {
public static void main(String[] args) {
Device tv = new TV();
Remote remote = new BasicRemote(tv);
remote.togglePower(); // TV включён
remote.volumeUp(); // Громкость TV: 15%
Device radio = new Radio();
remote = new BasicRemote(radio);
remote.togglePower(); // Radio включено
remote.volumeUp(); // Громкость Radio: 15%
}
}
Преимущество: мы можем добавлять новые типы устройств или новые пульты, не меняя существующий код. Паттерн Bridge идеально подходит для кроссплатформенных приложений (например, UI на разных ОС).
2. Visitor (Посетитель) — новые операции без изменения классов
Паттерн Visitor позволяет добавлять новые операции к группе объектов, не изменяя сами классы этих объектов. Это классический способ обойти ограничения одинарной диспетчеризации в Java.
Когда применять: когда у вас есть стабильная иерархия классов, но вы часто добавляете новые операции (например, экспорт в разные форматы, генерация отчётов).
Пример: Экспорт фигур в разные форматы
У нас есть фигуры (Circle, Square) и мы хотим экспортировать их в JSON и XML.
// Элементы (Elements)
interface Shape {
void accept(Visitor visitor);
}
class Circle implements Shape {
public double radius = 5.0;
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Square implements Shape {
public double side = 4.0;
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// Посетитель (Visitor)
interface Visitor {
void visit(Circle circle);
void visit(Square square);
}
class JsonExportVisitor implements Visitor {
@Override
public void visit(Circle circle) {
System.out.println("{ \"type\": \"circle\", \"radius\": " + circle.radius + " }");
}
@Override
public void visit(Square square) {
System.out.println("{ \"type\": \"square\", \"side\": " + square.side + " }");
}
}
class XmlExportVisitor implements Visitor {
@Override
public void visit(Circle circle) {
System.out.println("<circle radius=\"" + circle.radius + "\"/>");
}
@Override
public void visit(Square square) {
System.out.println("<square side=\"" + square.side + "\"/>");
}
}
// Использование
public class VisitorDemo {
public static void main(String[] args) {
Shape[] shapes = {new Circle(), new Square()};
Visitor json = new JsonExportVisitor();
Visitor xml = new XmlExportVisitor();
for (Shape shape : shapes) {
shape.accept(json);
shape.accept(xml);
}
}
}
Результат: нам не нужно менять классы Circle или Square, чтобы добавить поддержку нового формата. Достаточно создать нового посетителя.
3. Memento (Хранитель) — сохранение и восстановление состояния
Паттерн Memento позволяет сохранять и восстанавливать предыдущее состояние объекта, не нарушая инкапсуляцию. Это основа для реализации Undo/Redo в приложениях.
Когда применять: когда нужно реализовать функциональность отмены действий, сохранения промежуточных состояний или создания чекпоинтов.
Пример: Текстовый редактор с Undo
// Memento (снимок состояния)
class EditorMemento {
private final String content;
public EditorMemento(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
// Originator (создатель снимка)
class Editor {
private String content = "";
public void write(String text) {
content += text;
}
public EditorMemento save() {
return new EditorMemento(content);
}
public void restore(EditorMemento memento) {
content = memento.getContent();
}
public void show() {
System.out.println("Содержимое: " + content);
}
}
// Caretaker (управляющий историей)
class History {
private final Stack<EditorMemento> stack = new Stack<>();
public void push(EditorMemento memento) {
stack.push(memento);
}
public EditorMemento pop