Привет, Хабр!

Сегодня разберём паттерн «Фрактальный декоратор» — способ рекурсивного декорирования объектов, позволяющий динамически добавлять уровни логики без изменения базового кода.

Суть паттерна в том, что он расширяет стандартный декоратор, позволяя одному декоратору оборачивать другой. То есть помимо базового компонента можно добавлять дополнительные слои декорирования, где каждый уровень вносит свою логику (например, логирование, проверку безопасности или измерение производительности). Кроме того, фрактальный декоратор обеспечивает рекурсивный вызов дочерних декораторов после выполнения базового метода.


Интерфейс и базовый компонент

Как и в любом хорошем паттерне, начнём с определения контракта для компонентов.

using System;

namespace FractalDecoratorDemo
{
    // Интерфейс, задающий контракт выполнения
    public interface IComponent
    {
        void Execute();
    }

    // Простейшая реализация, которая выполняет основную бизнес-логику
    public class ConcreteComponent : IComponent
    {
        public void Execute()
        {
            Console.WriteLine("ConcreteComponent: Выполняю основную бизнес-логику...");
        }
    }
}

Здесь всё предельно очевидно: базовый компонент знает, как выполнить своё дело, а декораторы будут его «приправлять».

Абстрактный фрактальный декоратор

В отличие от классического паттерна, здесь декоратор не просто оборачивает компонент, он может содержать список дочерних декораторов, вызываемых рекурсивно.

using System;
using System.Collections.Generic;

namespace FractalDecoratorDemo
{
    // Абстрактный класс декоратора, который реализует IComponent
    public abstract class FractalDecorator : IComponent
    {
        // Компонент, который оборачивается данным декоратором.
        protected readonly IComponent _component;

        // Коллекция дочерних декораторов для рекурсивного вызова
        protected readonly List<FractalDecorator> _children = new List<FractalDecorator>();

        protected FractalDecorator(IComponent component)
        {
            _component = component ?? throw new ArgumentNullException(nameof(component));
        }

        // Добавляем дочерний декоратор
        public void AddDecorator(FractalDecorator child)
        {
            if (child == null)
                throw new ArgumentNullException(nameof(child));
            _children.Add(child);
        }

        // Главный метод Execute, реализующий рекурсивный вызов дочерних декораторов
        public virtual void Execute()
        {
            PreExecute();
            _component.Execute();
            foreach (var child in _children)
            {
                child.Execute();
            }
            PostExecute();
        }

        // Виртуальные методы для дополнительной логики до и после выполнения
        protected virtual void PreExecute() { }
        protected virtual void PostExecute() { }
    }
}

Обратите внимание, что здесь используем композицию и рекурсию — идеальное сочетание для создания фрактальных структур. Каждый декоратор имеет возможность добавить свои дочерние обёртки.

Конкретные реализации

Теперь создадим пару конкретных декораторов.

Логирующий декоратор

«Если что‑то идёт не так — смотри в логи!». Поэтому первым делом добавим декоратор, который будет логировать начало и окончание выполнения.

using System;

namespace FractalDecoratorDemo
{
    // Декоратор для логирования
    public class LoggingFractalDecorator : FractalDecorator
    {
        public LoggingFractalDecorator(IComponent component) : base(component) { }

        protected override void PreExecute()
        {
            Console.WriteLine("[LOG] Начало выполнения компонента...");
        }

        protected override void PostExecute()
        {
            Console.WriteLine("[LOG] Завершение выполнения компонента.");
        }
    }
}

Декоратор производительности

Следующий декоратор будет измерять время выполнения. Никто не любит медленные системы, так что давайте узнаем, сколько времени тратится на выполнение кода.

using System;
using System.Diagnostics;

namespace FractalDecoratorDemo
{
    // Декоратор для измерения производительности
    public class PerformanceFractalDecorator : FractalDecorator
    {
        private Stopwatch _stopwatch;

        public PerformanceFractalDecorator(IComponent component) : base(component) { }

        protected override void PreExecute()
        {
            _stopwatch = Stopwatch.StartNew();
        }

        protected override void PostExecute()
        {
            _stopwatch.Stop();
            Console.WriteLine($"[PERF] Выполнение заняло {_stopwatch.ElapsedMilliseconds} мс.");
        }
    }
}

Декоратор обработки исключений

Ошибки случаются, поэтому полезно добавить декоратор, который будет ловить и обрабатывать исключения.

using System;

namespace FractalDecoratorDemo
{
    // Декоратор для обработки исключений
    public class ExceptionHandlingFractalDecorator : FractalDecorator
    {
        public ExceptionHandlingFractalDecorator(IComponent component) : base(component) { }

        public override void Execute()
        {
            try
            {
                base.Execute();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[ERROR] Произошло исключение: {ex.Message}");
                // Здесь можно добавить логику по уведомлению или другим мерам
            }
        }
    }
}

Декоратор проверки безопасности

Ещё один пример — декоратор, который проверяет права доступа перед выполнением основного метода.

using System;

namespace FractalDecoratorDemo
{
    // Декоратор для проверки безопасности
    public class SecurityFractalDecorator : FractalDecorator
    {
        public SecurityFractalDecorator(IComponent component) : base(component) { }

        protected override void PreExecute()
        {
            if (!UserHasAccess())
            {
                throw new UnauthorizedAccessException("Доступ запрещен!");
            }
            Console.WriteLine("[SECURITY] Доступ подтверждён.");
        }

        // Простейшая проверка прав доступа (в реальных системах логика будет сложнее)
        private bool UserHasAccess()
        {
            // Для примера всегда возвращаем true
            return true;
        }
    }
}

Комбинируем декораторы

Теперь скомбинируем несколько декораторов и добавим дочерние декораторы к основному декоратору.

using System;

namespace FractalDecoratorDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1. Создаём базовый компонент
            IComponent baseComponent = new ConcreteComponent();

            // 2. Оборачиваем базовый компонент в декораторы:
            //    Сначала добавляем декоратор безопасности, потом измерение производительности, затем логирование
            IComponent securedComponent = new SecurityFractalDecorator(baseComponent);
            IComponent perfComponent = new PerformanceFractalDecorator(securedComponent);
            IComponent loggingComponent = new LoggingFractalDecorator(perfComponent);

            // 3. Теперь превращаем это в фрактальную структуру, добавляя дочерние декораторы
            //    Создаём корневой декоратор – возьмём логирующий как базовый
            var rootDecorator = loggingComponent as FractalDecorator;
            if (rootDecorator == null)
            {
                Console.WriteLine("Не удалось создать корневой декоратор!");
                return;
            }

            // Добавляем дочерние декораторы:
            // Например, дополнительный декоратор для обработки исключений
            rootDecorator.AddDecorator(new ExceptionHandlingFractalDecorator(baseComponent));
            // Ещё один декоратор производительности, который будет вызван рекурсивно
            rootDecorator.AddDecorator(new PerformanceFractalDecorator(baseComponent));

            // Для наглядности добавим ещё один уровень вложенности:
            // Создаём декоратор логирования, который сам будет содержать дочерний декоратор обработки исключений
            var nestedDecorator = new LoggingFractalDecorator(new PerformanceFractalDecorator(baseComponent));
            nestedDecorator.AddDecorator(new ExceptionHandlingFractalDecorator(baseComponent));
            rootDecorator.AddDecorator(nestedDecorator);

            // Выполняем всю фрактальную цепочку декораторов
            Console.WriteLine("=== Старт фрактального выполнения ===");
            rootDecorator.Execute();
            Console.WriteLine("=== Завершение фрактального выполнения ===");

            Console.WriteLine("Нажмите любую клавишу для выхода...");
            Console.ReadKey();
        }
    }
}

Сначала оборачиваем компонент в Security, затем в Performance и Logging‑декораторы: это позволяет проверить права доступа, измерить время выполнения и залогировать процесс. После этого добавляем дочерние декораторы к корневому логирующему, демонстрируя рекурсивное поведение фрактального паттерна, где каждый уровень может содержать свои обёртки, создавая практически бесконечное число комбинаций. Если возникает ошибка, ExceptionHandlingFractalDecorator ловит её и выводит сообщение.


А вы использовали фрактальные декораторы в своих проектах? Если да, то как? Делитесь в комментариях.

20 февраля пройдёт открытый урок «Трейсинг запросов в .NET с использованием Jaeger v2».

На нём вы узнаете, как организовать мониторинг запросов в .NET-приложениях, как настроить Jaeger для трейсинга запросов и как анализировать трассированные данные в Jaeger UI. Записаться можно на странице курса «C# ASP.NET Core разработчик».

Комментарии (4)


  1. VADemon
    18.02.2025 18:45

    Как будет выглядеть освобождение ресурсов? Например, внешний декоратор что-то выделил в Pre-execution, а в Post бы освободил. Но внутренний декоратор выкинул исключение. Post-Execute же не будет вызван?


    1. Evengard
      18.02.2025 18:45

      Это можно поидее решить оверрайдом Execute-а и try/finally если сильно нужно, если я правильно понял


      1. VADemon
        18.02.2025 18:45

        Тогда получается, что "Декоратор обработки исключений" обязателен и, чтобы не писать лапшу, у каждого уже исполненного декоратора он должен попытаться дернуть cleanup/handleException? А чтобы отработать по списку в обратном порядке надо либо в стэк записывать, либо текущий индекс списка декораторов пробрасывать и его с i до 0 проходить?

        Пересмотрел код и комментарий, сам по себе оверрайд execute не поможет, потому что исключение может выпасть в соседнем блоке, см. дальше:

        Хотя, если бы здесь действительно был рекурсивный алгоритм вызова декораторов (а не по списку, см. "// Главный метод Execute, реализующий рекурсивный вызов дочерних декораторов"), можно было бы обойтись try-catch на каждом(!) шагу, где нужен cleanup? Потом изнутри, как матрешка, исключение пробрасывается на верх.


  1. Evengard
    18.02.2025 18:45

    У ChildExecutor-ов получается не будет возможности оверрайднуть выполнение, в отличие от классических декораторов, поскольку они вообще по сути на вход не получают "внутренние" компоненты. Да и получается что PreExecute для child-ов будет выполнен на самом деле уже после выполнения основного компонента... Странно это как-то.

    Я честно говоря ожидал бы какого-нибудь построения рекурсивных делегатов или что-то вроде того