Метод программирования, именуемый аспектно-ориентированным, впервые явился миру в конце девяностых годов прошлого века, когда группа исследователей из Xerox PARC под руководством Грегора Кичалеса решила, что объектно-ориентированного подхода человечеству недостаточно. Они создали AspectJ — расширение для Java, призванное разрешить проблему, которую окрестили «сквозной функциональностью». Суть проблемы проста до безобразия: код логирования, обработки ошибок, проверки прав доступа и прочих служебных радостей размазывается по всему приложению, как масло по по́лу, превращая элегантную бизнес-логику в свалку повторяющихся конструкций.
Аспектно-ориентированное программирование предлагает выделить эти сквозные concerns в отдельные сущности — аспекты, которые можно применять к коду декларативно, не засоряя основную логику техническими деталями. В теории звучит как серебряная пуля. На практике AspectJ оказался инструментом, требующим от программиста понимания магических pointcut expressions и готовности смириться с тем, что код компилируется через специальный компилятор, производящий байткод, который отладить можно только с поллитрой, бубном или молитвенником.
Весь этот балаган приключился лет тридцать тому назад. С тех пор аспектно-ориентированное программирование обрело статус «той штуки, о которой все слышали, но никто не использует», заняв почётное место рядом с CORBA и XML-конфигами на три тысячи строк. Причин тому множество: сложность инструментария, непрозрачность происходящего, отсутствие нормальной поддержки в IDE, и главное — ощущение, что ты буквально продал душу дьяволу ради избавления от десятка строк boilerplate-кода.
Недавно ради старого знакомого я вписался в аудит восьмилетнего финтеховского проекта на дотнете — и, ну как бы это помягче… В общем, метрики они не собирали. Аудит сотен тысяч строк кода на неродном языке без visibility — удовольствие то еще, да и я привык отвечать за результат в продакшене, а не в мертвой ветке гитхаба. Короче, я начал с телеметрии; если бы я расставлял вызовы ручками — я бы отъехал в дурку на третий день. Поэтому я познакомился с Metalama — библиотекой, которая обещает вернуть аспектно-ориентированному программированию утраченную прелесть (я был фанатом AOP еще двадцать с лишним лет назад, я даже в руби его затащил). Создатели Metalama, видимо, внимательно изучили все ошибки предшественников и решили сделать AOP таким, каким он должен был быть с самого начала: понятным, прозрачным и работающим непосредственно с исходным кодом C#, а не с какими-то абстрактными байткодами или IL-инструкциями.
Metalama работает на этапе компиляции, генерируя C#-код, который можно увидеть, отладить и понять без степени в компьютерной магии. Библиотека интегрируется с Roslyn — и предоставляет API для создания аспектов, которые модифицируют код во время сборки. Ключевое отличие от AspectJ и прочих динозавров: всё происходит в терминах C#, без изобретения собственных DSL и синтаксических конструкций, понятных только посвящённым.
Вот вам несколько примеров.
Логирование (телеметрия устроена так же)
Классический подход — скопировать строчку с вызовом логгера в начало каждого метода, а затем провести остаток дня, проклиная бренность бытия и отсутствие метакопипаста. Вот как это выглядит без аспектов:
public class OrderService { private readonly ILogger _logger; public void CreateOrder(Order order) { _logger.LogInformation("CreateOrder called with {OrderId}", order.Id); // Бизнес-логика создания заказа _logger.LogInformation("CreateOrder completed"); } public void CancelOrder(string orderId) { _logger.LogInformation("CancelOrder called with {OrderId}", orderId); // Бизнес-логика отмены заказа _logger.LogInformation("CancelOrder completed"); } // И так далее для каждого из ста методов... }
С Metalama вы создаёте аспект LogAttribute, помечаете им нужные методы или целые классы, и библиотека автоматически добавляет код логирования при компиляции:
public class LogAttribute : OverrideMethodAspect { public override dynamic? OverrideMethod() { var methodName = meta.Target.Method.Name; var parameters = meta.Target.Parameters.ToArray(); Console.WriteLine($"Entering {methodName}"); try { var result = meta.Proceed(); Console.WriteLine($"Leaving {methodName} successfully"); return result; } catch (Exception ex) { Console.WriteLine($"Exception in {methodName}: {ex.Message}"); throw; } } }
Теперь применяем аспект:
[Log] public class OrderService { public void CreateOrder(Order order) { // Только бизнес-логика, без захламления логированием } public void CancelOrder(string orderId) { // То же самое — чистый код (в смысле, без артефактов логирования) } }
Metalama сгенерирует весь необходимый код логирования автоматически. Магия? Нет, метапрограммирование, но такое, которое не вызывает желание сменить профессию.
INotifyPropertyChanged: бич WPF-разработчиков
Другой классический пример — реализация INotifyPropertyChanged для UI-приложений. Каждый, кто писал на WPF или Xamarin, знает эту боль. Без аспектов каждое свойство превращается в трёхстрочную конструкцию:
public class PersonViewModel : INotifyPropertyChanged { private string _firstName; public string FirstName { get => _firstName; set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } private string _lastName; public string LastName { get => _lastName; set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // И ещё двадцать восемь свойств... }
С Metalama достаточно пометить класс атрибутом, и весь boilerplate генерируется автоматически:
[NotifyPropertyChanged] public class PersonViewModel { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } // Все свойства — автоматические, без backing fields и OnPropertyChanged }
Внезапно класс с тридцатью свойствами умещается на экран, а не растягивается на три страницы.
Валидация параметров: defensive programming без занудства
Проверка входных параметров — ещё один источник бесконечного copy-paste. Классический подход выглядит так:
public class UserService { public void RegisterUser(string email, string password, int age) { if (string.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password)); if (age < 0) throw new ArgumentOutOfRangeException(nameof(age)); // Собственно логика регистрации } public void UpdateProfile(string userId, string bio) { if (string.IsNullOrEmpty(userId)) throw new ArgumentNullException(nameof(userId)); if (string.IsNullOrEmpty(bio)) throw new ArgumentNullException(nameof(bio)); // Логика обновления профиля } }
С Metalama создаём аспект для валидации:
public class NotNullAttribute : ContractAspect { public override void Validate(dynamic? value) { if (value == null || (value is string str && string.IsNullOrEmpty(str))) { var parameterName = meta.Target.Parameter.Name; throw new ArgumentNullException(parameterName); } } }
Применяем:
public class UserService { public void RegisterUser( [NotNull] string email, [NotNull] string password, [Range(0, 150)] int age) { // Только бизнес-логика } public void UpdateProfile([NotNull] string userId, [NotNull] string bio) { // Никаких проверок вручную } }
Архитектурная валидация: enforcement на стероидах
Metalama предлагает не только генерацию кода, но и его валидацию. Вы можете создавать аспекты, которые проверяют соблюдение архитектурных правил и выдают ошибки или предупреждения прямо в редакторе, не дожидаясь компиляции.
Например, хотите убедиться, что все публичные методы в контроллерах помечены атрибутами авторизации:
public class RequireAuthorizationAttribute : TypeAspect { public override void BuildAspect(IAspectBuilder<INamedType> builder) { foreach (var method in builder.Target.Methods.Where(m => m.Accessibility == Accessibility.Public)) { if (!method.Attributes.Any(a => a.Type.Name.Contains("Authorize"))) { builder.Diagnostics.Report( Diagnostic.Create( "ARCH001", "Public controller method must have authorization attribute", method)); } } } }
Применяем к базовому контроллеру:
[RequireAuthorization] public abstract class BaseController : ControllerBase { } public class UserController : BaseController { // Ошибка компиляции: метод без [Authorize] public IActionResult GetUsers() => Ok(); // Всё в порядке [Authorize] public IActionResult DeleteUser(string id) => Ok(); }
Это превращает аспекты из средства борьбы с boilerplate в инструмент принуждения к архитектурным стандартам, что особенно актуально в больших командах, где кодревью превращается в бесконечную войну с человеческим фактором.
Кэширование: ещё один пример
Добавление кэширования к методам — типичная задача, которая обычно требует обёртывания каждого метода в конструкцию проверки кэша:
public class CacheAttribute : OverrideMethodAspect { public override dynamic? OverrideMethod() { var cacheKey = $"{meta.Target.Type.Name}.{meta.Target.Method.Name}"; if (Cache.TryGet(cacheKey, out var cachedResult)) { Console.WriteLine($"Cache hit for {cacheKey}"); return cachedResult; } var result = meta.Proceed(); Cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); return result; } }
Применяем:
public class ProductService { [Cache] public List<Product> GetProducts() { // Дорогостоящая операция загрузки из базы return _repository.LoadAllProducts(); } }
Реальность использования
Библиотека поддерживает интеграцию с IDE, предоставляя немедленную обратную связь. Изменили аспект — увидели результат в редакторе, не перекомпилируя проект. Это кардинально меняет опыт разработки: вместо цикла «скопипастил-скомпилировал-ужаснулся-крякнул-отменил» (в просторечии: «ССУКО») — получаешь мгновенную визуализацию того, что аспект делает с кодом. Metalama даже предоставляет возможность просматривать сгенерированный код прямо в IDE, что превращает отладку из шаманского ритуала в обычную инженерную задачу. Да, даже в моём виме.
Конечно, в этом мире живут не только радужные пони и веселые единороги. Метапрограммирование всегда несёт риск создания кода, который сложно понять человеку, впервые открывшему проект. Аспект может модифицировать код неочевидным образом, и новый разработчик потратит час, разбираясь, откуда в методе взялась строчка, которой нет в исходнике. Это плата за избавление от boilerplate, и платить её придётся документацией, именованием и архитектурной дисциплиной. Metalama пытается минимизировать этот риск прозрачностью — сгенерированный код доступен для просмотра, аспекты пишутся на обычном C#, но фундаментальную проблему это не решает: магия остаётся магией, даже если она документирована и предсказуема.
Ещё один момент: Metalama работает на этапе компиляции, что означает увеличение времени сборки. Для небольших проектов это незаметно, но в крупных энтерпрайзных монстрах каждая секунда компиляции превращается в вечность, особенно когда вы в сотый раз за день пересобираете проект после правки опечатки. Создатели библиотеки оптимизируют производительность, но законы физики обмануть сложно: метапрограммирование требует анализа и генерации кода, сиречь — времени.
Best practices
Тем не менее, Metalama представляет собой наиболее зрелую и практичную реализацию идей аспектно-ориентированного программирования для экосистемы .NET. Библиотека не пытается революционизировать парадигму программирования, не изобретает новые синтаксические конструкции и не требует переписывать весь проект с нуля. Она просто предлагает элегантный способ избавиться от повторяющегося кода и следует принципу принудительных архитектурных правил (нет телеметрии — нет метода в продакшене), используя привычные инструменты и подходы.
Хорошие практики при использовании Metalama сводятся (как всё и как всегда, в принципе) к здравому смыслу: не злоупотребляйте аспектами там, где достаточно обычного наследования или композиции; документируйте аспекты подробно, объясняя, что они делают и почему; держите аспекты простыми и фокусируйте каждый на одной задаче; используйте валидацию для проверки архитектурных инвариантов; и главное — помните, что аспекты это инструмент, а не религия. Иногда три строки повторяющегося кода лучше, чем аспект, который нужно поддерживать и объяснять каждому новому члену команды.
Аспектно-ориентированное программирование прошло долгий путь от академических экспериментов Xerox PARC до практичного инструмента для промышленной разработки. Metalama демонстрирует, что идеи, заложенные в AspectJ четверть века назад, могут работать, если подойти к их реализации прагматично, без фанатизма и с уважением к разработчикам, которым потом этот код поддерживать. Библиотека не спасёт мир и не превратит плохого программиста в хорошего, но она может сделать жизнь разработчика чуть менее монотонной, избавив от необходимости в тысячный раз писать одну и ту же конструкцию. А это уже немало.
