Метод программирования, именуемый аспектно-ориентированным, впервые явился миру в конце девяностых годов прошлого века, когда группа исследователей из 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 четверть века назад, могут работать, если подойти к их реализации прагматично, без фанатизма и с уважением к разработчикам, которым потом этот код поддерживать. Библиотека не спасёт мир и не превратит плохого программиста в хорошего, но она может сделать жизнь разработчика чуть менее монотонной, избавив от необходимости в тысячный раз писать одну и ту же конструкцию. А это уже немало.
