Если вы считаете, что объектно-ориентированное программирование предназначено для старшего поколения разработчиков…
И функциональное программирование звучит круто для вас, но все эти монады кажутся слишком сложными.
У меня есть потенциальное решение для вас!
Аспектно-ориентированное программирование (АОП)! Это спасет отрасль!

Я просто шучу!
АОП — не очередной чемпион на поле боя (слава Богу…).
На самом деле АОП был придуман для поддержки ООП.
В этой статье мы рассмотрим, что такое АОП и как он может изменить и потенциально улучшить ваш проект.
Как именно АОП поддерживает нас?
Во многих проектах мы можем найти следующую проблему.
- Мы определили модули на основе нашей бизнес-области.
- Эти модули должны удовлетворять функциональные требования нашего приложения.
- В идеальном мире это все, за что они должны нести ответственность.
К сожалению, это не так просто.
У нас может быть много дополнительных требований, таких как добавление журналов о том, что происходит в приложении, или понимание производительности определенных функций.
Эти типы дополнений можно определить как Сквозные проблемы. Они не относятся к этим вертикальным модулям, но как-то в них появляются. Конечно, в примере с добавлением механизма логирования — у нас мог бы быть LoggerModule — но Logger из этого модуля все равно должен запускаться внутри бизнес-модулей.
Пример, который мы демонстрируем, является распространенной проблемой, которую мы могли бы решить с помощью АОП.
Общая идея состоит в том, чтобы добавить дополнительное поведение к существующему коду, не изменяя его.
Следовательно, мы должны указать, какой код мы хотим обогатить.
Это позволяет добавлять в программу поведение, не являющееся центральным для бизнес-логики, не загромождая основной код этой дополнительной (несвязанной) функциональностью.
АОП уже есть!
Аспектно-ориентированное программирование (АОП) не должно быть чем-то новым для тех, кто уже работает с NestJS.
Вполне возможно, что вы уже используете его.
Интересно, где?
В документации Nest мы можем найти следующую главу:
Перехватчики обладают набором полезных возможностей, вдохновленных техникой аспектно-ориентированного программирования (АОП). Они позволяют:
привязать дополнительную логику до/после выполнения метода
преобразовать результат, возвращаемый функцией
преобразовать исключение, выброшенное из функции
расширить базовое поведение функции
полностью переопределить функцию в зависимости от конкретных условий (например, для целей кэширования)
И это еще не все!
Эти пункты описывают общую идею того, что дает нам аспектно-ориентированное программирование, которое применимо к перехватчикам, а также к охранникам, каналам и большинству декораторов уровня протокола.
так что это не ограничивается REST API, а доступно везде!
Пользовательские аспекты
Эта статья только о том, что уже есть в документации?
Нисколько!
Хорошо, что NestJS уже предоставляет готовые аспекты для большинства случаев, зависящих от фреймворка, но он не может охватить все сквозные проблемы, которые могут у нас возникнуть.
Чтобы охватить уникальные и специфические ситуации, мы должны научиться писать собственные аспекты.
Рассмотрим следующий сценарий.
Мы создаем приложение для приюта для животных. Одной из основных функций станет возможность завести домашнего питомца. Поток для этого варианта использования может быть реализован внутри AdoptService.
На данный момент не стоит читать курс. У него слишком много обязанностей и строк кода. Код этого класса выглядит как этот.
Помимо очевидной ответственности (предоставление функциональности внедрения) он имеет дело с 3 дополнительными сквозными проблемами:
- Регистрация выполнения и результата.
- Регистрация ошибок во внешнем сервисе (в данном случае Datadog).
- Мониторинг производительности.
Наша цель — упростить эту услугу, убрав эти сквозные проблемы.
Мы можем разделить его на два случая.
Логика, которая не требует фреймворка
Этот случай намного проще. Мы говорим здесь о дополнительной логике, которая может существовать в простой функции. Нам не нужно использовать какую-то сложную логику, которая живет в провайдерах, зарегистрированных в приложении.
Для всех случаев будем использовать декораторы. Они позволяют нам делать что-то дополнительное с той частью кода, которую мы украшаем.
Это все, что нам нужно в данном случае.
Мы можем обогатить наш сервис приложения с помощью декоратора и добавить дополнительное ведение журнала. Это возможно, поскольку регистратор NestJS можно легко создать без использования контейнера Dependency Injection.
Декоратор может просто выглядеть вот так.
Логика, которая требует фреймворка
Этот случай намного сложнее. Мы не можем делать все из декоратора, так как у нас нет доступа к провайдерам из него.
Чтобы понять, что нам нужно сделать, давайте проверим это на диаграмме.
- На этот раз мы будем использовать декоратор только для того, чтобы пометить наш класс или метод для обогащения. Мы сделаем это, добавив к нему метаданные.
- В дальнейшем наш сервис будет зарегистрирован приложением как обычно.
- Затем мы должны определить класс Explorer, который будет находить отмеченные места в приложении.
- Все найденные методы передаются в класс, который дополняет их, поэтому он отвечает за то, чем был декоратор в более простом случае, но на этот раз это провайдер, поэтому мы можем внедрить все, что нам нужно, через конструктор. Например, поставщик Datadog.
Чтобы проверить, как выглядит реализация этой схемы, загляните в этот репозиторий.
Какие преимущества мы можем получить?
Более простой код
После переноса трех сквозных задач из предыдущего примера мы получаем гораздо меньший сервис, и, что наиболее важно, он содержит только точный код, необходимый для реализации функции внедрения.
Меньше шаблонов
Сквозные задачи имеют такое свойство, как то, что они должны быть реализованы во многих местах.
В большинстве случаев они почти одинаковы во всех тех местах, что заставляет нас просто копировать-вставлять эту часть кода и делать нашу работу скучной.
Более безопасный рефакторинг
Наличие некоторой логики во многих местах приложения приводит к другой проблеме.
Предположим, что API Datadog изменился, и метод registerError теперь требует дополнительного свойства.
К сожалению, это означает, что мы должны изменить каждое место, где мы использовали этот класс.
Не лучше ли обновить только то место, где мы обогащаем декорированные методы?
В конце концов, оба случая безопасны, если приложение правильно протестировано. Это?
Более простое тестирование
Без использования АОП код, относящийся к сквозным проблемам, существует между бизнес-логикой. Худшее, что мы можем сделать, — это протестировать все вместе в одних и тех же тест-кейсах, как в юнит-тестах для AdoptService. В том случае, когда мы сталкиваемся с необходимым рефакторингом, как описано выше, мы также должны обновить тесты. Если мы изменим тесты, связанные с функциональностью внедрения, мы не можем быть уверены, что она по-прежнему работает, поэтому мы не уверены в нашем рефакторинге.
Когда мы используем АОП, ничто не побуждает нас смешивать эти тесты вместе.
Мы пишем тесты для аспекта только в одном месте и, возможно, какие-то дополнительные тесты, которые проверяют, правильно ли он подключен. Никакие модификации аспектов не затрагивают тесты для AdoptService.
Переключение функций
Во втором сценарии код обогащается нашими новыми провайдерами. Вероятно, они предоставляются модулем. Что будет, если мы не импортируем этот модуль?
Ничего страшного, этот модуль отлично инкапсулирован, и кто-то извне использует его напрямую. Когда мы не импортируем модуль, Explorer.onModuleInit не будет выполняться и аспект не будет назначен. Например, мы можем сделать его зависимым от переменной среды, и мы уже реализовали переключатель функций для всего аспекта.
Какой может быть вариант использования?
Мы хотим выпустить наше новое приложение в рабочую среду. Перед этим мы должны оценить производительность приложения, поэтому мы добавляем множество показателей для наиболее важных функций.
Надеюсь, мы подготовили аспект «Метрики», как описано выше.
Тесты пройдены, и теперь мы уверены в нашем программном обеспечении. Мы можем отключить аспект с помощью переменной среды, и мы готовы получить клиентов.
Каковы риски использования АОП?
Использование АОП для бизнес-логики
Преобразование функциональных требований в аспекты может стать большой проблемой.
Это часто реализуется с помощью хуков After/Before Insert/Update и т. д. или чего-то вроде подписчиков TypeORM.
Помимо того факта, что АОП был изобретен для решения сквозных проблем (а не бизнес-правил), из-за кода здесь мы можем столкнуться со многими проблемами, такими как:
- Мы не отвечаем за то, происходит ли это изменение в той же транзакции, что и добавление пользователя.
- Обработка ошибок и компенсация вне контекста того, почему пользователь был создан
- Что, если в какой-то момент мы не будем создавать кошелек при каждом создании пользователя? Возможно, мы определили новый тип пользователя.
- Такие зависимости сложнее отслеживать при рефакторинге. Может стать сюрпризом, что когда мы переводим управление пользователями на Auth0 или другой ORM, кошельки больше не работают.
И многое другое.
Для такой логики события и их слушатели должны быть лучше. Может быть, помимо простого CRUD, где у нас действительно есть единственный и простой способ создать пользователя.
Использование экспериментальных декораторов
Весь Nest зависит от реализации TypeScript раннего предложения декораторов. Это решение может иметь неприятные последствия, как вы можете прочитать в этой теме. Это может означать, что добавлять их больше — не лучшая идея, тем более что никто не будет заботиться о том, чтобы помочь вам перейти на какой-либо новый стандарт.
Будем надеяться, что декораторы — не единственный способ указать модулю, какие части кода следует обогатить, например
or
Нестандартная реализация
Как вы можете видеть в указанном репозитории, вам нужно написать много кода, чтобы ввести пользовательские аспекты. Это возлагает на вас ответственность за его поддержание.
И многое другое
- Более сложная отладка
- Небезопасные модификации объекта
- Мы можем упустить некоторые детали переопределенного метода, такие как его старые метаданные,
- и т. д.
Краткое содержание
Аспектно-ориентированное программирование было изобретено для решения общих проблем, существующих в ООП. Мы уже успешно используем его, когда речь идет об уровне API, и у нас есть возможность добиться такого же успеха в других частях наших приложений.
Однако…

Худшим исходом для этой статьи было бы, если бы вы только вспомнили, как реализовать аспект, а затем начали использовать его везде.
Так что, может быть, и хорошо, что эта реализация не так проста (хотя могла бы быть и проще). Это заставит нас дважды подумать, действительно ли мы хотим пойти на этот компромисс, чтобы получить преимущества АОП, и решение будет лучше продуманным и более информированным.