Decorator (Декоратор)
Decorator позволяет динамически добавлять поведение объекту.
Когда нам нужно расширить функциональность, первая мысль – добавить логику в существующий класс или создать его наследника. Но что если мы добавим поведение не всему классу, а лишь объекту этого класса?
Пример
Представим, что архитектура приложения построена так, что каждое действие представлено командой и обработчиком команды.
Например, есть команда «создать заказ»:
package order;
import java.util.List;
public record PlaceOrderCommand(UserId userId, List<ProductId> items) implements Command {
public PlaceOrderCommand {
items = List.copyOf(items);
}
}javaИ есть её обработчик:
package order;
public final class PlaceOrderHandler implements CommandHandler {
private final OrderRepository orderRepository;
public PlaceOrderHandler(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void handle(Command command) {
// В реальном коде обработчик обычно типизирован через generics
var cmd = (PlaceOrderCommand) command;
var order = Order.place(cmd.userId(), cmd.items());
orderRepository.save(order);
}
}javaКоманда и обработчик реализуют интерфейсы:
package order;
public interface CommandHandler {
void handle(Command command);
}javapackage order;
public interface Command {}javaПомимо PlaceOrderHandler в приложении есть и другие обработчики:
RegisterUserHandler– зарегистрировать пользователя.PayForOrderHandler– оплатить заказ.CancelOrderHandler– отменить заказ.
Представим, что мы захотели логгировать начало и окончание выполнения каждой команды.
Прямолинейный вариант – добавить логгирование в каждый обработчик:
package naive;
import java.util.logging.Logger;
public final class PlaceOrderHandler implements CommandHandler {
private static final Logger logger = Logger.getLogger(PlaceOrderHandler.class.getName());
private final OrderRepository orderRepository;
public PlaceOrderHandler(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void handle(Command command) {
logger.info("Started handling command: " + command.getClass().getSimpleName());
var cmd = (PlaceOrderCommand) command;
var order = Order.place(cmd.userId(), cmd.items());
orderRepository.save(order);
logger.info("Finished handling command: " + command.getClass().getSimpleName());
}
}javapackage naive;
import java.util.logging.Logger;
public final class RegisterUserHandler implements CommandHandler {
private static final Logger logger = Logger.getLogger(RegisterUserHandler.class.getName());
@Override
public void handle(Command command) {
logger.info("Started handling command: " + command.getClass().getSimpleName());
// Логика регистрации пользователя
logger.info("Finished handling command: " + command.getClass().getSimpleName());
}
}javaПроблема очевидна – у нас множество обработчиков, и мы в каждом дублируем один и тот же код логгирования. Помимо этого мы усложняем понимание бизнес-логики, поскольку логгирование напрямую к ней не относится.
Решение – создать класс-декоратор:
package order;
import java.util.logging.Logger;
public final class LoggingHandlerDecorator implements CommandHandler {
private final CommandHandler decorated;
private final Logger logger;
public LoggingHandlerDecorator(CommandHandler decorated, Logger logger) {
this.decorated = decorated;
this.logger = logger;
}
@Override
public void handle(Command command) {
logger.info("Started handling command: " + command.getClass().getSimpleName());
try {
decorated.handle(command);
} finally {
logger.info("Finished handling command: " + command.getClass().getSimpleName());
}
}
}javaКласс реализует тот же интерфейс, что и обработчики – CommandHandler. За счёт этого мы можем «обернуть» любой обработчик в LoggingHandlerDecorator:
var placeOrderHandler = new LoggingHandlerDecorator(
new PlaceOrderHandler(new OrderRepository()),
logger
);
var registerUserHandler = new LoggingHandlerDecorator(
new RegisterUserHandler(),
logger
);javaТеперь представим, что некоторые команды работают медленно и мы хотим замерять время и потребление памяти. Создадим новый декоратор:
package order;
import java.util.logging.Logger;
public final class ProfilingHandlerDecorator implements CommandHandler {
private final CommandHandler decorated;
private final Logger logger;
public ProfilingHandlerDecorator(CommandHandler decorated, Logger logger) {
this.decorated = decorated;
this.logger = logger;
}
@Override
public void handle(Command command) {
long startTime = System.nanoTime();
// Упрощённый замер – freeMemory() может давать некорректные значения
// из-за сборки мусора. В production используйте JMX или профайлер
long startMemory = Runtime.getRuntime().freeMemory();
try {
decorated.handle(command);
} finally {
long elapsed = System.nanoTime() - startTime;
long memoryUsed = startMemory - Runtime.getRuntime().freeMemory();
logger.info(String.format(
"Command %s: time=%dms, memory=%d bytes",
command.getClass().getSimpleName(),
elapsed / 1_000_000,
memoryUsed
));
}
}
}javavar payForOrderHandler = new ProfilingHandlerDecorator(
new LoggingHandlerDecorator(
new PayForOrderHandler(),
logger
),
logger
);javaЗаметим, что мы можем оборачивать декоратор другим декоратором. Всё за счёт того, что все декораторы реализуют один и тот же интерфейс – CommandHandler.
Декораторы позволяют динамически добавлять поведение к объектам. Мы можем создавать любую комбинацию из декораторов.
Реальные примеры
BufferedInputStream
java.io.FilterInputStream – базовый декоратор для InputStream в стандартной библиотеке Java. Он наследует InputStream, оборачивает другой InputStream и делегирует ему все вызовы.
На его основе построены конкретные декораторы: BufferedInputStream добавляет буферизацию, DataInputStream – чтение примитивных типов. Декораторы можно комбинировать:
import java.io.*;
// FileInputStream – конкретный компонент
InputStream file = new FileInputStream("data.bin");
// BufferedInputStream – декоратор: добавляет буферизацию
InputStream buffered = new BufferedInputStream(file);
// DataInputStream – декоратор: добавляет чтение примитивных типов
DataInputStream data = new DataInputStream(buffered);
int value = data.readInt();
double price = data.readDouble();javaКлиентский код, который ожидает InputStream, может работать с любой комбинацией декораторов – ему не нужно знать, сколько слоёв обёрнуто вокруг исходного потока.
Collections.unmodifiableList
Collections.unmodifiableList() возвращает декоратор, который оборачивает список и запрещает его модификацию. Операции чтения (get, size, contains) делегируются оригинальному списку, а операции записи (add, remove, set) выбрасывают UnsupportedOperationException.
import java.util.*;
List<String> original = new ArrayList<>(List.of("один", "два", "три"));
// unmodifiableList – декоратор: запрещает модификацию
List<String> readOnly = Collections.unmodifiableList(original);
readOnly.get(0); // "один" – чтение работает
readOnly.size(); // 3
readOnly.add("четыре"); // UnsupportedOperationExceptionjavaio.LimitReader
io.LimitReader из стандартной библиотеки Go оборачивает io.Reader и ограничивает количество байт, которое можно из него прочитать. Функция принимает io.Reader и возвращает io.Reader – классический декоратор.
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
source := strings.NewReader("Привет, мир! Это длинная строка.")
// io.LimitReader – декоратор: ограничивает количество читаемых байт
limited := io.LimitReader(source, 12)
data, err := io.ReadAll(limited)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data)) // Привет
}goПо тому же принципу работает io.TeeReader – он оборачивает Reader и дублирует все прочитанные данные в Writer. Декораторы можно комбинировать:
var buf bytes.Buffer
// TeeReader – декоратор: дублирует данные в buf при чтении
// LimitReader – декоратор: ограничивает чтение до 1024 байт
reader := io.LimitReader(io.TeeReader(source, &buf), 1024)go