Pull to refresh

Мой опыт с AOP и PostSharp

Reading time 3 min
Views 11K
Давным-давно я достаточно хорошо исследовал PostSharp и даже начал использовать его в своих разработческих целях. Умудрился даже написать скринкаст и пару-тройку статей на эту тему. К сожалению, с PostSharp’ом у меня в последствии совсем не сложилось и, по причинам периодических воспросов со стороны комьюнити на тему “как? почему?”, я решил написать этот пост чтобы расставить все точки над i.

Бесплатный сыр в мышеловке


PostSharp позволяет втыкать функционал в существующий код путем переписывания IL-а. Эта затея ничем не нова, и написать MSBuild-овый Task который будет делать ILDASM, что-то в IL менять, и делать потом ILASM — задача достаточно тривиальная. В свое время я именно это делал для того чтобы удалять метаданные обфускаторов из сборки. (Для меня все еще является загадкой, зачем обфускаторы намеренно помогают кракерам, рассказывая что именно они были использованы.)

Суть PostSharp в том, что можно декларативно взять и пометить, например, метод, сказав что каждый его вызов будет происходить в отдельном потоке:

[WorkerThread]
public void MyMethod()
{
  ⋮
}

PostSharp в процессе посткомпиляции берет этот метод и переписывает его, заменяя простой вызов метода на какой-нибудь Task.Factory.StartNew(() => { /* тут метод */ });. Это выглядит неплохо и, будем честны, работает.

Но есть и ряд проблем. Например, таким кодом сложно управлять. То есть, я не могу вдруг взять и сказать, что в отдельном потоке теперь будут выполняться только методы класса А, а методы класса Б будут блокируищими. Более того, я не могу пользователям моего API толком объяснить, что метод реализует нестандартное поведение – они будут продолжать думать, что метод блокирует, и писать асинхронные вызовы поверх него, что ни к чему хорошему не приведет.

Насчет документирования со мной могут поспорить – дескать раз аттрибут присутствует, то всем должно быть все понятно. Но тут все тоже не так просто – мне например категорически не нравится что аттрибут присутствует вообще после компиляции. И это было проблемой PostSharp – он пытался тянуть за собой свою библиотеку, в то время когда рациональный подход к вопросу наталкиывает на мысль, что по-хорошему никакие библиотеки не нужны, т.к. у нас идет локальное переписывание IL, и все.

Эффект привыкания


Был у меня огромный класс, помеченный атрибутом [NotifyPropertyChanged] – думаю вы уже поняли что он делал. И все было бы как бы хорошо, но на практике меня беспокоило несколько вещей.

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

Во-вторых, сам объект на этапе компиляции не реализовывал INotifyPropertyChanged и соответственно доступ к этому интерфейсу приходилось ‘подделывать’ через двойной каст.

В третьих, подход с атрибутом был all-or-nothing. Например, я мог получать нотификации на read-write свойства, но когда мне захотелось чтобы изменение такого свойства еще и генерировало NotifyPropertyChanged() на зависимое от него read-only свойство, тут начались проблемы.

В результате, я сделал простую вещь: удалил PostSharp из проекта и сделал реализацию INPC через ReSharper. Иначе говоря, я тупо автоматизировал процесс создания кода, прогнал его по всем свойствам, а потом вручную подкорретировал те из них, от которых мне понадобилось что-то особенное.

Альтернативы?


Мне кажется, все что угодно лучше чем переписывание IL вручную. Идеалом для меня является использование объектной модели компилятора для манипуляции структурами, т.е. фактически подход при котором происходит расширение компилятора. Язык программирования Boo позволяет это делать, Nemerle тоже, ну а что касается C# 5, то тут никто никаких гарантий дать не может – мы не знаем какие механизмы метапрограммирования будут в следующей версии.

Другой вариант – это кодогенерация. Приемущество ее в том, что по крайней мере видно что происходит, т.к. мы компилируем только код, без какой-либо post-build магии. Естественно, кодогенерация работает лучше всего когда она использует какой-то парсер вроде встроенного в ReSharper. Через API ReSharper'а можно творить весьма безумные вещи по манипуляции кодом. Например, проблема нотификаций на зависимые переменные легко решается на уровне отдельного демона (анализатора), который может шагать по зависимостям и подсказывать нам, что и где нужно добавить.

Вариант третий – это сделать свою DSL. Если задача не ложится на простой C#, возможно ей не место в нем. Напишите DSL на том же F#, или на каком-то еще языке, или воспользуйтесь например MPS чтобы задизайнить эту DSL и производить из нее все что угодно. Да, это та же кодогенерация только под другим углом, но по крайней мере такой подход менее обманчив: он не пытается втиснуть несовместимые концепции в ограничения того или иного языка.

На этом все. Прошу заметить, что мое отношение к AOP/PostSharp это всего лишь мое личное мнение. Если у вас все работает – это супер, напишите об этом! ■
Tags:
Hubs:
+22
Comments 13
Comments Comments 13

Articles