BackendBit

К списку паттернов

Decorator позволяет динамически добавлять поведение объекту.

Когда нам нужно расширить функциональность, первая мысль – добавить логику в существующий класс или создать его наследника. Но что если мы добавим поведение не всему классу, а лишь объекту этого класса?

UML диаграмма классов паттерна ДекораторПоказывает интерфейс CommandHandler с методом handle(Command), реализованный классом PlaceOrderHandler и двумя декораторами: LoggingHandlerDecorator и ProfilingHandlerDecorator. Декораторы оборачивают другой CommandHandler.<<interface>>CommandHandler+handle(Command): voidPlaceOrderHandlerLoggingHandlerDecorator+decorated: CommandHandlerProfilingHandlerDecorator+decorated: CommandHandlerКонкретный обработчикДекораторы – оборачиваютдругой CommandHandler

Пример

Представим, что архитектура приложения построена так, что каждое действие представлено командой и обработчиком команды.

Например, есть команда «создать заказ»:

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);
}
java
package 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());
    }
}
java
package 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
            ));
        }
    }
}
java
var 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("четыре"); // UnsupportedOperationException
java

io.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