ДИЗАЙН УЗОРЫ СЕРИИ

Как динамически добавлять поведения с помощью шаблона декоратора

Все, что вам нужно знать о Decorator

Что такое паттерн-декоратор?

Шаблон декоратора - это шаблон структурного проектирования, который позволяет динамически добавлять поведение к классу, не внося изменений в этот класс. Другими словами, он использует композицию вместо наследования для добавления функциональности объекта.

Элементы паттерна декоратора

IComponent

IComponent - это интерфейс. Это абстракция того, что мы хотим украсить. Вы также можете использовать абстрактный класс.

Составная часть

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

Декоратор

Класс декоратора будет реализовывать тот же интерфейс или абстрактный класс, что и класс Component.

Конструктор декоратора всегда принимает в качестве параметра объект, реализующий тот же интерфейс. Затем объект Component вводится в атрибут класса в конструкторе Component. Нравится.

Когда Decorator реализует интерфейс IComponent, вы должны реализовать методы SomeMethod() и AnotherMethod(). В эти методы вы можете добавить новое поведение.

Когда вы не вызываете внутренний метод Component, он больше не является шаблоном декоратора. Вы не добавите нового поведения. Вы запрограммируете новую реализацию интерфейса IComponent.

Множественные декораторы

Вы не ограничены только одним объектом Decorator для каждого класса Component. Вы можете определить декоратор, который украшает другой декоратор, который украшает компоненты, и так далее. Посмотрите на изображение ниже.

Оба изображенных декоратора должны реализовывать оба метода, определенные в интерфейсе IComponent.

Блок с именем Code представляет собой место в кодовой базе, где я работаю с Component.

Код всегда в первую очередь вызывает методы самого внешнего декоратора. Чтобы перейти в класс Component, вы должны вызвать одноименный метод внутреннего декоратора в каждом декораторе.

Если вы хотите использовать новое поведение обоих декораторов одновременно, вы должны изменить инициализацию объекта Component в вызывающем коде. Это можно сделать в три этапа.

  1. Инициализация DecoratorTwo.
  2. Инициализация DecoratorOne и передача в конструктор DecoratorTwo.
  3. Инициализация Component и переход в конструктор DecoratorOne.

Вот и все! Вам больше не нужно изменять код. Слово «динамический» приобретает свою ценность.

Руководство

Предпосылки

Учебное пособие предполагает базовые знания .NET и объектно-ориентированного программирования.

Введение в демонстрационный проект

Образец проекта представляет собой веб-приложение, которое собирает данные о местонахождении автомобилей Компании. Внешний API предоставляет данные.

Ваша роль в проекте

Представьте, что вы работаете в качестве разработчика службы поддержки, и за образец проекта вы отвечаете. Неожиданно вам звонит покупатель;

«Здравствуйте, мистер AwesomeReader, я хочу сообщить, что страница расположения данных заканчивается ошибкой тайм-аута».

А может, он не такой вежливый.

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

LocationService класс

Класс LocationService заботится о запросе API и сериализации собранных данных в объекты передачи данных.

Класс реализует интерфейс ILocationService. Интерфейс содержит определение метода GetLocation(). Реализация выглядит так.

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

Во-первых, это затруднит чтение класса. Что еще более важно, когда вы изменяете существующий код, вы можете быстро внести ошибку в приложение.

Еще одна причина не делать этого - нарушение принципа единой ответственности. Смысл принципа можно выразить одним предложением.

У класса должна быть только одна причина для изменения.

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

Принятие этого решения позволяет расширить LocationService без изменения класса. Это напоминает мне другой ТВЕРДЫЙ принцип, и это принцип открытости-закрыт. Основная мысль этого принципа звучит так.

Программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.

Декоратор по своей природе поддерживает принцип открытого-закрытого. Какой отличный образец.

Декоратор журналов

Вы собираетесь создать LoggingLocationService, который является декоратором вашего компонента LocationService.

Отлично. Я заметил, что вы используете встроенный класс секундомера для измерения прошедшего времени в миллисекундах. Это намного лучше моего решения по вычитанию двух переменных с разными DateTime.Now значениями.

Теперь вам нужно немного изменить вызывающий код, который вы можете найти в классе Index.cshtml.cs. Инициализация LocationService выглядит так.

Вам необходимо заменить new LocationService(apiKey] на

new LoggingLocationService(
  new LocationService(apiKey),
    _loggerFactory.CreateLogger<LocationService>())

вот и все. Вы готовы к работе.

История продолжается

К счастью, благодаря реализованному ведению журнала вы смогли найти причину длительного времени загрузки. Вы сотворили волшебство, как делают разработчики, и ошибки больше не существует.

Пока вы кодировали, вам в голову пришла дикая идея; «Может быть, я смогу кэшировать коллекцию ячеек в память и сократить время загрузки даже больше».

CachingLocationService

После разговора с товарищами по команде вы решаете кэшировать данные на один час. Звучит как разумное количество времени. Время для другого декоратора.

Хорошо выглядеть. Я вижу, вы используете встроенный класс MemoryCache. Кроме того, нет необходимости генерировать случайный ключ кеша, потому что вы загружаете всю коллекцию, верно?

В первом операторе if вы просто пытаетесь получить значение коллекции местоположений по ключу кеша. Если коллекция пуста, будут загружены новые данные. Следовательно, вы устанавливаете новую запись в кеш по истечении часа.

Теперь я думаю, вы хотите объединить оба новых декоратора. Регистрирующий декоратор и кэширующий декоратор вместе.

_locationService = new CachingLocationService(
  new LoggingLocationService(
    new LocationService(apiKey), logger), memoryCache);

Вы завершили инициализацию LoggingLocationService инициализацией CachingLocationService. Выглядит немного нечитабельно. Попробуйте использовать несколько переменных.

var withLocationService = new LocationService(apiKey);var withLoggingLocationService = new LoggingLocationService(withLocationService, logger);var cachingLocationService = new CachingLocationService(withLoggingLocationService, memoryCache)

Намного лучше. Когда я вижу код, в котором вы можете инициализировать экземпляр с различным поведением, я использую Builder Pattern. Вы можете изучить это, но не сейчас.

Итоговая диаграмма классов после наших изменений выглядит так.

Декоратор и внедрение зависимостей

Перед реализацией любого декоратора регистрация в контейнере LoggingService в проекте Sample выглядела как в примере ниже.

services.AddScoped<ILocationService>(serviceProvider => 
  new LocationService(apiKey));

В данном случае мы используем встроенный контейнер .NET Core IoC. Эта регистрация означает, что введенный ILocationService через конструктор всегда будет предоставлять экземпляр объекта LocationService. Но вам нужен экземпляр CachingLocationService.

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

Есть третий вариант. Вы можете использовать отличный NuGet-пакет под названием Scrutor. Это расширение внедрения зависимостей в .NET. Он добавляет методы регистрации, чтобы сделать регистрацию более удобочитаемой. Круто, правда?

Декоратор в .NET

Вы также можете найти шаблон декоратора во внутренностях .NET. Яркий пример - Класс Stream.

На вершине иерархии находится класс Stream. Это абстрактный класс, и вам необходимо предоставить реализации для нескольких методов, в основном для метода чтения и записи. Знакомо, правда?

Затем у нас есть несколько конкретных реализаций класса Stream, включая FileStream, MemoryStream и NetworkStream. Каждый из этих классов будет эквивалентен классу LocationService.

Наконец, у нас есть несколько классов, которые действуют как декораторы. К ним относятся классы BufferedStream, GZipStream и CryptoStream.

Как распознать, когда использовать декоратор?

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

Другой типичной ситуацией является модификация данных перед их передачей в Компонент. Например, зашифровать и расшифровать данные или преобразовать единицы.

Встроенный ярлык в Visual Studio

В Visual Studio есть встроенный инструмент рефакторинга, который можно использовать с помощью сочетания клавиш Ctrl + R, а затем Ctrl + I. Visual Studio создаст класс Decorator из выбранного интерфейса.

Резюме

Помните, что шаблоны - это просто направляющие рельсы, а не предписывающие правила реализации. Они не высечены в камне. Шаблон декоратора - динамичный, чистый, поддерживающий любимые принципы, и вы должны рассматривать его как новый инструмент в вашем наборе инструментов. Образец проекта можно найти на Github.