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

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

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

Рассмотрим следующий пример:

На первый взгляд, здесь нет ничего плохого. Мы внедряем реализации Logger (который, как оказалось, является интерфейсом, мы скоро увидим), так что это отметка в поле SOLID.

Однако есть некоторые улучшения, которые мы можем внести. Logger - это сквозная проблема. Он плетется через наше приложение, как плющ. Да, мы вводим его, но учтите бремя обслуживания. Если подпись метода log изменяется, это требует огромного объема рефакторинга.

Декораторы спешат на помощь

Если вы используете Angular, вы уже хорошо знакомы с декораторами в TypeScript, но если нет, не волнуйтесь - эта статья предполагает ровно нулевые знания. Итак, начнем с самого начала.

Декоратор «украшает» класс или метод и предоставляет некоторые дополнительные функции во время инициализации, то есть при создании экземпляра класса. В этом примере мы собираемся реализовать декоратор, который регистрирует сообщение при каждом вызове метода, чтобы мы могли удалить внедренные зависимости Logger и вызовы lo g.

В TypeScript декораторы выглядят так:

@decorator()
public class Foo { ... }

Хорошо, прежде чем мы перейдем к написанию декоратора, давайте сделаем некоторые настройки. Нам понадобятся некоторые функции ведения журнала. Это будет обрабатываться двумя классами: ConsoleLogger и LoggerFactory и интерфейсом Logger. Давайте посмотрим на них:

Интерфейс Logger имеет единственный метод, log, который принимает строку. Простой.

Класс ConsoleLogger реализует интерфейс Logger и записывает сообщение в консоль (в конце концов, это очень пример).

LoggerFactory отвечает за создание и возврат одного экземпляра ConsoleLogger. Если вы знакомы с шаблоном singleton, вы это поймете. Если нет - это быстрый и грубый пример реализации одноэлементного паттерна.

Правильно - настройка завершена. У нас есть средства входа в консоль. Теперь давайте посмотрим на реализацию декоратора.

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

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

В строке 3 мы используем LoggerFactory, чтобы получить экземпляр Logger, который мы будем использовать для записи сообщений в консоль. Пока все просто.

Веселье начинается на пятой строке.

Мы создаем функцию, которая возвращает функцию. «Внешняя» функция, объявленная в строке 5, на самом деле является фабрикой декораторов. Это функция, которая создает и возвращает фактический декоратор. Мы можем использовать фабрику декораторов, чтобы настроить способ применения декоратора, но в этом примере мы просто возвращаем декоратор.

Итак, сам декоратор. Это функция в строках 6, 7 и 8. Как видите, все, что она делает, это вызывает метод log. Этот тип декоратора является декоратором метода. Его следует применять только к методам класса.

Вы можете видеть, что есть три аргумента - target, propertyKey и дескриптор. Давайте определимся, что это:

  • target: класс, частью которого является член.
  • propertyKey: имя участника.
  • дескриптор: это, по сути, объект, который вы бы передали Object.defineProperty, если бы мы писали этот класс на JavaScript.

Хорошо, вот и наш декоратор запущен. Давайте реализуем это в классе Counter, который мы видели в самом начале статьи.

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

В строках 7 и 12 мы реализовали simpleLog (помните синтаксис @ для декоратора). Мы также смогли удалить внедренный регистратор из класса (ура, никаких сквозных проблем) и жестко запрограммированные вызовы log из каждого из методов.

Это будет работать так же, как и раньше, регистрируя, что мы вызываем каждый метод на каждой итерации.

Верно?

Посмотрим на результат…

❯ node index
Calling currentCount
Calling incrementCount
0
1
2
3
4
5
6
7
8
9

Хм…

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

Помните, в начале статьи мы узнали, что декораторы вызываются во время установки, а нет при вызове их декорированных методов. Вот что мы здесь видим. Экземпляр Counter создается, поэтому вызывается декоратор.

Итак, как мы можем воспроизвести ту функциональность, которая была у нас изначально.

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

Как и раньше, мы используем фабрику декораторов, которая возвращает функцию, и аргументы target, propertyKey и descriptor одинаковы. Разница заключается в самой функции декоратора.

В строке 3 мы создаем копию свойства value дескриптора. Это декорированный метод, который будет выполняться на самом деле.

Затем мы устанавливаем descriptor.value на новую функцию, которая обертывает оригинал. Вы можете увидеть это в строках с 5 по 8. В строке 6 мы вызываем log, а в строке 7 мы вызываем копию исходного метода и возвращаем результат.

Давайте реализуем это в index.ts и посмотрим, как будет выглядеть результат.

Единственное изменение - использовать наш новый декоратор полезный журнал.

И на выходе…

Calling currentCount
0
Calling incrementCount
Calling currentCount
1
Calling incrementCount
Calling currentCount
2
Calling incrementCount
Calling currentCount
3
Calling incrementCount
Calling currentCount
4
Calling incrementCount
Calling currentCount
5
Calling incrementCount
Calling currentCount
6
Calling incrementCount
Calling currentCount
7
Calling incrementCount
Calling currentCount
8
Calling incrementCount
Calling currentCount
9
Calling incrementCount

Намного лучше!

Заключение

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

Это был, конечно, очень простой пример, но он, надеюсь, дал вам представление о том, что такое декораторы и как они работают.

На данный момент (июнь 2020 г.) декораторы в JavaScript и, следовательно, TypeScript все еще находятся в экспериментальном состоянии. Их окончательная реализация на JS официально не согласована. Лично мне нравится использовать в TypeScript - где компилятор отлично справляется с абстрагированием экспериментальных функций, - но я не очень доволен их использованием в приложениях без транспонирования JavaScript.

При этом нам доступны очень мощные функциональные возможности, знакомые тем, кто из мира C # или Java, и которые действительно могут помочь решить некоторые проблемы (например, сквозные проблемы).