Аспектно-ориентированное программирование (AOP) или почему мы отказались от использования самого маститого вендора AOP – PostSharp. Часть 1
Введение
В этом цикле статей статей, посвященную аспектно-ориентированному программированию, мы расскажем, как наша компания (ITA Labs) пришла к использованию аспектно-ориентированного программирования (AOP) при разработке продуктов, зачем это нужно и почему это важно, какой путь мы прошли в использовании этого подхода, и как, в итоге, пришли к разработке собственного AOP решения, которое:
- Можно использовать в продуктах, подлежащих сертификации (когда продукт и его зависимости должны полностью собираться из исходных кодов)
- Построено на Open Source библиотеках, т.е. бесплатно
- Позволило бесшовно и прозрачно мигрировать существующие разработки с одного из самых известных продуктов в AOP-мире - PostSharp
Но обо всем по порядку.
Типовая функциональность
Большинство людей, а разработчики ПО – тем более, стараются исключить или свести к минимуму выполнение рутинных, повторяющихся операций. В разработке ПО к таким операциям можно отнести написание типового, шаблонного кода, т.н. кода «обвязки» вокруг непосредственной бизнес-логики приложения.
ITA Labs специализируется на разработке высоконагруженных серверных решений, поэтому вопрос унификации типового «обвязочного» кода для нас очень актуален. Перечислим основные типы вспомогательной функциональности, которые реализуются практически в каждом продукте компании, и являются, по сути, необходимым минимумом к реализации, помимо самой бизнес-логики:
- Трассировка выполнения кода
- Измерения производительности выполняемого кода
- Обработка ошибок в выполняемом коде
- Прикладная логика аутентификации / авторизации
- Запись диагностики работы продукта через Event Tracing for Windows (ETW)
Вспомогательную функциональность вокруг метода бизнес-логики можно представить в виде контуров, как показано на рисунке ниже:
В ходе развития компании росло количество и размеры проектов, и разработчики компании задумались над вопросом упрощения внедрения вспомогательной функциональности при разработке.
Какие цели мы перед собой ставили:
- Уменьшение дублирования кода (Упрощение поддержки и развития написанного кода)
- Разделение бизнес-логики и вспомогательной функциональности в кодовой базе (Упрощение поддержки и развития написанного кода)
- Ускорение процесса разработки (Снижение издержек на разработку)
- Унифицированная реализация вспомогательной функциональности для разрабатываемых в компании продуктов (Не нужно от проекта к проекту решать типовые задачи, заново изобретать «велосипед»)
Одним из вариантов в качестве достижения целей рассматривался такой прием в разработке как кодогенерация. Речь идет о прикладной кодогенерации в момент написания кода (design-time), когда к указанным файлам с исходными кодами применяется утилита (например, что-то самописное), которая по заранее определенным правилам модифицирует текст исходных кодов. Кодогенерация позволяет уменьшить количество ручных действий при написании кода, т.е. ускорить разработку, но при этом мы не получаем ни уменьшение дублирования кода, ни разделение бизнес-логики и вспомогательной функциональности. Поэтому использование кодогенерации нам не подходило.
В дальнейшем, в ходе изысканий (в основном в Сети, по материалам конференций и форумов) часто встречалось упоминание о решении задач, похожих на наши, с помощьюаспектно-ориентированного программирования. И мы решили погрузиться в эту тему.
Переход к АОП
Вспомогательную функциональность, используемую в наших продуктах, можно назвать сквозной, т.к. она не привязана к конкретному архитектурному слою. Например, трассировка, используется как в модуле доступа к данным, так и в модуле обработки данных.
Идея аспектно-ориентированного программирования как раз и заключается в выделении так называемой сквозной функциональности. Сквозная функциональность – это функциональность, реализация которой затрагивает несколько модулей. Эта функциональность не может быть выделена в отдельный модуль из-за ограничения возможностей языка программирования или выбранной архитектуры.
Рассмотрим основные понятия АОП:
Аспект – это реализация сквозной функциональности, выполненная в виде специального модуля,содержащего фрагменты кода (действия аспекта), активируемые в заданных точках целевой программы. Определение аспекта содержит также спецификацию среза кода целевой программы – совокупность правил поиска в ней точек присоединения для последующего внедрения в них действий аспекта.
Точка соединения – это точка в выполняемой программе, в которой должно быть выполнено действие аспекта.
Срез – это набор точек соединения. Срез определяет, подходит ли данная точка соединения к заданному аспекту на основании спецификации аспекта.
Аспект представляет собой атрибут, действия аспекта реализованы в методах атрибута OnEnter / OnExit (трассировка входа и выхода из метода / свойства). Спецификация атрибута указывает на то, что атрибут может применяться к классам, методам, и свойствам (перечисление AttributeUsage). Срез для аспекта определяется на основании того, к чему применен атрибут. Если атрибут применен к классу, то срез будет назначать точками соединения методы и свойства класса, и внедрять в них действия аспекта. Если атрибут применен к конкретным методам и свойствам, то срез будет меньше, и точками соединения будут указанные методы и свойства, а не все методы и свойства класса.
Ниже представлен рисунок, отображающий основные понятия АОП:
ITA Labs специализируется на разработке программного обеспечения под платформу .NET, поэтому нашей целью было найти АОП-фреймворк для .NET, позволяющий реализовать вспомогательный функционал для проектов через эту парадигму. Фреймворк должен был отличаться надежностью (для использования в коммерческой разработке), простотой использования, не влиять драматически на производительность продукта (т.е. генерировать минимум дополнительного кода) и возможностью отладки.
Выяснилось, что АОП-фреймворки можно разделить по подходу к реализации процесса внедрения действий аспектов в точки соединения:
- Прокси-классы, создаваемые в ходе выполнения программы (run-time)
- Модификация существующего кода (пост-обработка бинарных модулей)
Внедрение через прокси-классы
Подход заключается в том, что посредством Reflection создается новый тип, наследуемый от интерфейса, в который требуется внедрить аспекты, и этот тип реализует прозрачный прокси-класс, выполняющий аспекты перед и после вызова целевого метода или свойства. Прозрачный прокси-класс (transparentproxy) – это класс, который реализует интерфейс целевого класса и прозрачно подменяет его собой для остальных модулей, проксируя через себя все вызовы к методам и свойствам целевого класса.
Например, есть класс для работы с данными – DatabaseManager (IDatabaseManager). И для него в run-time средствами фреймворка (см. ниже) генерируется прокси-класс для логгирования (также реализующий интерфейс IDatabaseManager), который добавляет запись в лог перед каждым вызовом метода DatabaseManager.
Можно сказать, что прокси-класс фактически выполняет перехват вызовов к целевому классу, добавляя свою логику – логику аспекта. И фреймворки, реализующие данный подход, сводят задачу программиста к написанию перехватичка (interceptor) для целевого класса, в котором реализуется логика аспекта.
Пример кода перехватичка (фреймворк CastleDynamicProxy) в терминах языка C#:
Схематично пример встраивания прокси-класса показан на рисунке ниже:
Минусы такого подхода:
- Создание дополнительных прокси-классов – снижение производительности
- Затруднена отладка – из-за прозрачных прокси-классов
Примеры фреймворков, реализующие такой подход:
Castle Dynamic Proxy: http://www.castleproject.org/projects/dynamicproxy/
Microsoft Unity: https://msdn.microsoft.com/en-us/library/dn507438(v=pandp.30).aspx
Модификация существующего кода
Подход заключается в том, что после основной процедуры сборки бинарных модулей запускается процедура пост-обработки, которая с помощью Reflection выявляет точки соединения и модифицирует эти точки – вставляя непосредственно код вызова действий аспектов. На выходе получаем бинарные модули, уже с внедренными действиями аспектов.
Пример кода реализации аспекта логгирования с помощью PostSharp и его использование:
В результате запуска тестового приложения будет такой вывод в консоль:
Плюсы:
- Возможна отладка
- Нет значительного снижения производительности, т.к. нет динамического создания дополнительных объектов
Минусы:
- Увеличивается общее время сборки продукта
- Исполняемый код в бинарных модулях отличается от исходных кодов
Примеры фреймворков, реализующие такой подход:
PostSharp: https://www.postsharp.net/
Итоги
В результате изысканий, для реализации вспомогательной функциональности для продуктов компании был выбран PostSharp, как наиболее подходящий инструмент:
- Поддержка основной платформы (Microsoft .NET) и языка разработки (C#), используемого в компании
- Низкий порог вхождения, хорошая документированность
- Удобство встраивания в процедуру сборки
- Возможность отладки реализуемого функционала
- Бесплатное использование (в части того функционала, который мы планировали использовать)
- Популярный и развивающийся проект с большим сообществом
PostSharp встраивается в сборку продукта и выполняет пост-обработку бинарных модулей, получаемых с помощью C#-компилятора:
- PostSharp интегрируется в процедуру сборки на уровне MSBuild.
- В процессе пост-обработки бинарников, PostSharp сначала выполняет дизассемблирование и анализ кода, а затем – трансформацию кода (трансформация выполняется на уровне промежуточного языка - MSIL) и перезапись бинарников в финальном виде.
Более подробно об использовании PostSharp, его преимуществах и недостатках, причинах отказа от PostSharp и переходу на решение собственной разработки – в следующих статьях.
Автор статьи: Илья Мосов, Технический директор, ITA Labs