Искусство есть посредник того, чего нельзя высказать.

Иоганн Вольфганг фон Гёте

Введение

Шаблоны проектирования - это важные инструменты для разработчиков программного обеспечения, позволяющие создавать многократно используемый и сопровождаемый код. Двумя популярными паттернами являются паттерн “Прокси” и паттерн “Декоратор”. Хотя эти два паттерна довольно похожи, у них разное применение и назначение. В этой статье мы рассмотрим различия между паттернами Proxy и Decorator.

Отличия Proxy от Decorator

Шаблон Proxy используется в качестве прослойки для другого объекта, чтобы контролировать доступ к нему. Прокси знает класс которому он принадлежит, обычно создает его сам и может использовать определенные методы этого класса, например, для подготовки данных.

В отличие от Proxy, паттерн Decorator используется для динамического добавления поведения к объекту без изменения его интерфейса. Декоратор работает только с декларируемым интерфейсом, то есть у него нет доступа к базовой реализации объекта.

Хотя эти паттерны имеют различные цели, их часто путают друг с другом. Люди часто не понимают различий между ними, что приводит к путанице.

Популярные причины путаницы:

  • Оба паттерна подразумевают оборачивание объекта в объект. В паттерне Decorator этот обертка добавляет новое поведение или функциональность исходному объекту, а в паттерне Proxy обертка действует как посредник для контроля доступа к исходному объекту. Такое сходство в структуре может затруднить разработчикам проведение различий между этими двумя паттернами.

  • Паттерн Decorator иногда называют “динамическим прокси”, что может еще больше размыть различия между Proxy и Decorator. Хотя они оба подразумевают добавление нового поведения к объекту, Decorator делает это, оборачивая объект одним или несколькими декораторами, в то время как Proxy предоставляет суррогат для исходного объекта.

Пример

Пример, включает интерфейс IValueProvider, который определяет одно свойство Value.

public interface IValueProvider
{
    public string Value { get; }
}

SlowValueProvider - это реализация этого интерфейса, имитирующая источник данных с большой задержкой при получении значения, перед получением значения вызывается метод LoadData, который использует публичное свойство Url, его нет в наследуемом интерфейсе.

public class SlowValueProvider : IValueProvider
{
    public string Url { get; set; } = "localhost:8080";
    
    public string Value => LoadData();

    private string LoadData()
    {
        Thread.Sleep(TimeSpan.FromMinutes(1));
        return $"Hello world ({Url})";
    }
}

Чтобы добиться увеличения производительности, вызванного SlowValueProvider, мы будем использовать кэширование для хранения результатов предыдущих запросов к свойству Value. Это значительно сократит время, необходимое для получения данных, в результате чего приложение станет более отзывчивым и эффективным.

Первая реализация - это кэширующий прокси. Кэширующий прокси - это объект, который находится между клиентом и реальным объектом и перехватывает обращения к объекту, кэшируя результаты этих обращений.

Вот пример того, как может быть реализован кэширующий прокси:

public class CacheProxyValueProvider : IValueProvider
{
    private readonly Lazy<string> _lazyValueProvider;

    public CacheProxyValueProvider(string url)
    {
        _lazyValueProvider = new Lazy<string>(() =>
        {
            var slowValueProvider = new SlowValueProvider
            {
                // Используем специфичный функционал
                Url = url
            };
            return slowValueProvider.Value;
        });
    }

    public string Value => _lazyValueProvider.Value;
}

В этом примере класс CacheProxyValueProvider реализует интерфейс IValueProvider и создаёт экземпляр SlowValueProvider, а в качестве аргумента конструктора принимает Url для загрузки данных. Так как наш Proxy знает конкретную реализацию IValueProvider, он может использовать специфичные свойства и методы для этого объекта, которых нет в интерфейсе. В нашем случае, мы можем менять Url, которого нет в интерфейсе.

Вторая реализация - это кэширующий декоратор. Он похож на прокси, но работает только с декорируемой абстракцией, в нашем случае он принимает фабрику по созданию IValueProvider.

Вот пример того, как может быть реализован декоратор:

public class CacheDecoratorValueProvider : IValueProvider
{
    private readonly Lazy<string> _lazyValue;

    public CacheDecoratorValueProvider(Func<IValueProvider> factory)
    {
        _lazyValue = new Lazy<string>(() =>
        {
            // Фабрика может возвращать всё что угодно (хоть прокси, хоть логгирующий декоратор или ещё что-то)
            var provider = factory();
            return provider.Value;
        });
    }

    public string Value => _lazyValue.Value;
}

В этом примере класс CacheDecoratorValueProvider принимает фабрику по созданию IValueProvider и поэтому не может использовать особенности реализации, но он может принимать в себя всё что угодно, что реализует интерфейс IValueProvider, это в свою очередь значит, что это может быть описанный выше CacheProxyValueProvider или другие декораторы.

Итог

В заключение следует отметить, что хотя паттерны Proxy и Decorator имеют некоторые общие черты, у них разные случаи использования и реализации. Шаблон Proxy используется для контроля доступа к объекту, добавления функциональности или предоставления упрощенного интерфейса, а шаблон Decorator используется для динамического добавления поведения объекту без изменения его интерфейса.

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

Ссылки