Decorator: добавляем поведение без наследования
Decorator: добавляем поведение без наследования
Логирование, кэш, метрики - поведение, которое хочется добавлять снаружи, а не зашивать в исходный объект. Decorator делает это аккуратно и без взрыва классов.
Проблема
У тебя есть репозиторий пользователей - PgUserRepo. Нужно добавить логирование. Потом - метрики. Потом - кэширование. Как, не нарушая SRP и не превращая репозиторий в комбайн?
Вариант 1: всё запихать в PgUserRepo - получится монстр на 500 строк, который и в БД ходит, и логирует, и кэширует.
Вариант 2: наследование - LoggingPgUserRepo extends PgUserRepo, потом CachingLoggingPgUserRepo extends LoggingPgUserRepo... Взрыв классов.
Вариант 3: Decorator - обёртки, которые добавляют поведение по одному слою.
Решение: Decorator
Decorator - обёртка, которая реализует тот же интерфейс, что и оборачиваемый объект. Она добавляет поведение до/после вызова, делегируя основную работу «внутреннему» объекту. Каждый слой остаётся с одной ответственностью, а добавление нового слоя не трогает существующие - это OCP в действии.
Декоратор логирования для репозитория
type UserRepo interface {
Get(id int64) (User, error)
}
// Основная реализация
type PgUserRepo struct{ db *sql.DB }
func (r *PgUserRepo) Get(id int64) (User, error) {
// SELECT * FROM users WHERE id = $1
return User{}, nil
}
// Декоратор логирования
type LoggingUserRepo struct {
next UserRepo
logger *slog.Logger
}
func (r *LoggingUserRepo) Get(id int64) (User, error) {
start := time.Now()
u, err := r.next.Get(id) // делегируем основную работу
r.logger.Info("UserRepo.Get",
"id", id,
"duration_ms", time.Since(start).Milliseconds(),
"error", err,
)
return u, err
}
Ключевой момент: LoggingUserRepo реализует UserRepo и содержит поле next UserRepo. Он не знает, что внутри - PgUserRepo, кэш или другой декоратор.
Стек декораторов
Декораторы складываются как матрёшка:
// Собираем стек: pg → cache → logging
var repo UserRepo
repo = &PgUserRepo{db: db}
repo = &CachingUserRepo{next: repo, cache: redis}
repo = &LoggingUserRepo{next: repo, logger: logger}
// Внешний код видит просто UserRepo
user, err := repo.Get(42)
// Путь: logging → caching → pg (или из кэша)
interface UserRepo
{
public function get(int $id): array;
}
final class PgUserRepo implements UserRepo
{
public function get(int $id): array
{
return ['id' => $id];
}
}
final class LoggingUserRepo implements UserRepo
{
public function __construct(
private readonly UserRepo $next,
private readonly LoggerInterface $logger,
) {}
public function get(int $id): array
{
$t = microtime(true);
try {
return $this->next->get($id);
} finally {
$ms = (int) ((microtime(true) - $t) * 1000);
$this->logger->info('UserRepo.get', ['id' => $id, 'ms' => $ms]);
}
}
}
Ключевой момент: LoggingUserRepo реализует тот же UserRepo и принимает другой UserRepo в конструктор. Он не знает, что внутри.
Стек декораторов
Декораторы складываются как матрёшка:
$repo = new PgUserRepo($pdo);
$repo = new CachingUserRepo($repo, $cache);
$repo = new LoggingUserRepo($repo, $logger);
// Внешний код видит просто UserRepo
$user = $repo->get(42);
// Путь: logging → caching → pg (или из кэша)
В Symfony такой стек собирается автоматически через #[AsDecorator(decorates: UserRepo::class)].
class PgUserRepo {
async get(id) {
// SELECT * FROM users WHERE id = $1
return { id };
}
}
// Декоратор логирования
class LoggingUserRepo {
#next;
#logger;
constructor(next, logger) {
this.#next = next;
this.#logger = logger ?? console;
}
async get(id) {
const start = Date.now();
try {
return await this.#next.get(id);
} finally {
this.#logger.info?.('UserRepo.get', { id, ms: Date.now() - start });
}
}
}
Ключевой момент: LoggingUserRepo имеет тот же метод get(id), что и PgUserRepo, и принимает другой репозиторий в конструктор. Он не знает, что внутри.
Стек декораторов
Декораторы складываются как матрёшка:
let repo = new PgUserRepo(db);
repo = new CachingUserRepo(repo, cache);
repo = new LoggingUserRepo(repo, logger);
// Внешний код видит просто репозиторий
const user = await repo.get(42);
// Путь: logging → caching → pg (или из кэша)
В JS такой стек часто собирают через простую функцию-обёртку (особенно для middleware) - withLogging(withCache(repo)). Это та же идея, только без явного класса. ES-decorators (@logging) - отдельная история на уровне синтаксиса класса, но семантически - то же самое.
Каждый слой добавляет одну обязанность. Порядок можно менять - например, логировать до или после кэша.
Middleware - это Decorator
HTTP middleware в Go - классический Decorator. То же в PHP: PSR-15 MiddlewareInterface оборачивает RequestHandlerInterface, цепочка собирается из обёрток.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // делегируем
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isAuthed(r) {
http.Error(w, "unauthorized", 401)
return // не делегируем - прерываем цепочку
}
next.ServeHTTP(w, r)
})
}
// Стек middleware = стек декораторов
handler := AuthMiddleware(LoggingMiddleware(myHandler))
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
final class LoggingMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
$t = microtime(true);
try {
return $handler->handle($request); // делегируем
} finally {
$ms = (int) ((microtime(true) - $t) * 1000);
$this->logger->info(sprintf(
'%s %s %d ms',
$request->getMethod(),
(string) $request->getUri(),
$ms,
));
}
}
}
final class AuthMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly ResponseFactory $responses,
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
if (!$this->isAuthed($request)) {
return $this->responses->error(401, 'unauthorized');
// не делегируем - прерываем цепочку
}
return $handler->handle($request);
}
private function isAuthed(ServerRequestInterface $request): bool
{
return $request->hasHeader('Authorization');
}
}
// Стек middleware = стек декораторов
$pipeline = new Pipeline([new AuthMiddleware($responses), new LoggingMiddleware($logger)], $myHandler);
// Express-стиль: middleware = (req, res, next) => { ... next(); }
function logging(req, res, next) {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.url} ${Date.now() - start} ms`);
});
next(); // делегируем
}
function auth(req, res, next) {
if (!req.headers.authorization) {
res.status(401).send('unauthorized');
return; // не делегируем - прерываем цепочку
}
next();
}
// Стек middleware = стек декораторов
app.use(auth);
app.use(logging);
app.get('/me', myHandler);
Каждый middleware принимает следующее звено и возвращает ответ - контракт сохраняется, поведение добавляется. Это и есть Decorator.
io.Reader chain: Decorator в stdlib
Go stdlib построен на цепочках io.Reader:
file, _ := os.Open("data.txt") // io.Reader
buffered := bufio.NewReader(file) // декоратор: добавляет буферизацию
limited := io.LimitReader(buffered, 1024) // декоратор: ограничивает размер
Каждый io.Reader оборачивает предыдущий, добавляя одну функцию. Это Decorator в чистом виде.
Другие примеры из stdlib:
io.LimitReader(r, n) - ограничивает количество байт
io.TeeReader(r, w) - читает из r и копирует в w
cipher.StreamReader{S, R} - декоратор шифрования поверх Reader
gzip.NewReader(r) - декоратор распаковки
Decorator vs наследование
| Decorator (композиция) | Наследование |
|---|---|
| Добавляет поведение в runtime | Фиксировано при компиляции |
| Слои комбинируются свободно | Жёсткая иерархия |
| Каждый слой - отдельная структура | Один «жирный» потомок |
| Работает в Go ✅ | Не работает в Go ❌ |
Когда НЕ использовать
Один слой, не растёт - если логирование нужно в одном месте, проще добавить log.Printf рядом с вызовом.
Декоратор меняет контракт - если обёртка изменяет сигнатуру или семантику метода, это уже не Decorator (скорее Adapter).
Слишком глубокий стек - 5+ слоёв декораторов затрудняют отладку. Если отслеживать вызов через 7 обёрток - что-то пошло не так.
Связь с другими паттернами
Decorator + Strategy - декоратор может использовать стратегию для логики
Decorator + Factory - фабрика собирает стек декораторов
Decorator + Proxy - Proxy контролирует доступ, Decorator добавляет поведение
(структурно похожи, разница в намерении)
Мини-задание
- Добавь логирование к интерфейсу через декоратор, не трогая реализацию
- Собери стек из 2 декораторов (логирование + метрики) для одного репозитория
- Напиши HTTP middleware, которое добавляет заголовок
X-Request-Id- это Decorator - Попробуй
io.TeeReader- прочитай файл и одновременно скопируй вos.Stdout