Pull to refresh

Аспектно-ориентированное программирование: изучи и сделай сам!

Reading time 17 min
Views 58K
Original author: Guirec Le Bars
Статья родилась из того, что мне потребовался удобный и простой механизм перехвата для некоторых задач, который легко реализуется техниками АОП. Существует довольно много перехватчиков (Casle.net, Spring.net, LinFu и т.д.), требующих внедрять динамические дочерние классы в IL-код во время исполнения и приводящих практически всегда к одним и тем же ограничениям, накладываемым на перехватываемые классы: не статические, не запечатанные, методы и свойства должны быть виртуальными и т.д…

Другие механизмы перехвата требовали изменения процесс сборки или покупки лицензии. Ни то ни другое я себе позволить не мог…

Введение


АОП – Аспектно-Ориентированное Программирование. Думаю, все знакомы с тем, что значит ОП, потому нужно разобраться, что же такое Аспект. Не волнуйтесь, об этом в статье позже.

Я постараюсь писать эту статью на уровне новичка. Единственное требование для дальнейшего чтения – знание концепции Объектно-Ориентированного Программирования.

Мне кажется, что понимание концепции позволит вам лучше понять реализацию. Но я также понимаю, что статья довольно большая, потому если вам стало скучно или вы утомились, всё-таки посмотрите часть с реализацией. К теории всегда можно вернуться.

Опытные разработчики, прочтите!


Если вы уже знакомы с АОП, не уходите!
Давайте я прямо здесь и сейчас раскрою, что же в этой статье…

Я расскажу о технике перехвата, которая позволит перехватывать:
• Любой класс (включая запечатанные, статичные, значимого типа)
• Конструкторы
• Инициализаторы типов
• Методы, свойства и события экземпляров (даже если они не помечены как виртуальные)
• Статичные методы, свойства и события

Без:
• Перекройки вашего кода и сборок
• Внедрения в IL-код
• Динамического создания чего-либо
• Изменения целевых классов
• Необходимости виверами (weaver) реализовывать что-либо (например MarshalByRef)

В целом, разговор пойдет о чистом управляемом коде, который можно запустить и на .Net 1.1 (вообще, я использую немного Linq, но вы можете от него легко избавиться) и перехватывать всё, что взбредет вам в голову.

Проясним окончательно. С помощью этой техники:
• Вы сможете перехватывать такие конструкции как System.DateTime.Now и System.IO.File!
• У вас не будет обычных ограничений характерных для всех популярных библиотек перехвата.
Всё еще сомневаетесь? Тогда читаем дальше!

Принципы АОП


Вводная


Некоторые могут подумать, что их путь познания Объектно-Ориентированного Программирования ещё не завершён, и есть ли смысл переключаться с ООП на АОП, отказываясь от всех практик изученных за годы тяжкого обучения? Ответ прост: не надо переключаться. Нет противостояния ООП и АОП! АОП – концепция, чьё название, по-моему, вводит в заблуждения. Понимание принципов АОП требует от вас работы с классами, объектами, наследованием, полиморфизмом, абстракциями и т.п., потому у вас никак не получится потерять всё то, чем вы пользуетесь в ООП.

В стародавние времена, когда интернет состоял из желтых страниц, досок объявлений и юзнета, лучше всего было читать книжки, чтобы изучить что-то (ремарка для нового поколения: книга – штука, состоящая из сшитых вместе бумажных листов, содержащих текст). И практически все эти книги постоянно напоминали вам о следующих правилах ООП:
• Правило №1: необходима инкапсуляция данных и кода
• Правило №2: никогда не нарушай Правило №1

Инкапсуляция была основной целью, ради которой появилось ООП в языках 3-го поколения (3GL).

Из вики:
Инкапсуляция используется для поддержки двух схожих, но различных, требований и, иногда, к их комбинации:
• Ограничение доступа к некоторым компонентам объекта.
• Языковая конструкция, облегчающая объединение данных с методами (или другими функциями), работающими с этими данными.


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

Уловили? Собственно, это всё что вам требовалось из теории. Отлично!

Теперь, перейдем к двум появившимся вопросам:
• Когда это «иногда»?
• Зачем это нужно?

О пользе АОП… некоторые сценарии


Сценарий А

Вы разработчик в банке. В банке прекрасно работает система. Бизнес стабилен. Правительство издает указ, требующий от банков большей прозрачности. При любом движении средств в банк или из него, это действие должно быть запротоколировано. Так же в правительстве говорят, что это только первый шаг на пути к прозрачности, будут еще.

Сценарий Б

Ваше веб-приложение выдано команде тестеров. Все функциональные тесты прошли, а приложение сломалось на нагрузочном тесте. Имеется нефункциональное требование, говорящее, что никакая страница не может обрабатываться на сервере более 500 мс. Проведя анализ, вы обнаружили десятки запросов к базе, которых можно избежать кэшируя результаты.

Сценарий В

Вы два года собирали доменную модель в идеальную библиотеку, содержащую 200+ классов. Позже вы узнаете, что для приложения пишется новый фронт-енд и необходимо все ваши объекты связать с UI. Но для решения задачи необходимо, чтобы все классы реализовывали INotifyPropertyChanged.

Приведенные примеры демонстрируют, где АОП может быть спасением. Все эти сценарии похожи в следующем:

Внедряемые действия (cross-cutting concern)

Когда это «иногда»?

Некоторым классам (банковской системы, сервисов доступа к данным, доменной-модели) необходимо получить функциональность, которая, в общем, не является «их делом».

• Дело банковской системы – переводить деньги. Протоколирование операций хочет правительство.
• Сервис доступа к данным нужен для получения данных. Кэширование данных – нефункциональное требование.
• Классы доменной модели реализуют бизнес-логику вашей компании. Оповещение UI о изменении свойства требуется только UI.

В целом, речь идёт о ситуациях, когда требуется написать код для различных классов для решения задач неприсущих этим классам. Говоря на диалекте АОП – нужны внедряемые действия.

Понимание внедряемых действий является ключевым для АОП. Нет внедряемых действий – нет надобности в АОП.

Зачем это нужно?
Давайте рассмотрим детально Сценарий В.

Проблема в том, что у вас, допустим, в среднем 5 свойств в каждом классе. Имея 200+ классов, вам придется реализовать (скопипастить) более 1000 шаблонных кусков кода, для превращения чего-то типа этого:
public class Customer
{
    public string Name { get; set; }
}

Во что-то типа такого:
public class Customer : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
        
    private string _Name;
    public string Name 
    { 
        get { return _Name; }
        set
        {
            if (value != _Name)
            {
                _Name = value;
                SignalPropertyChanged("Name");
            }
        }
    }

    void SignalPropertyChanged(string propertyName)
    {
        var pcEvent = this.PropertyChanged;
        if (pcEvent != null) pcEvent(this, new PropertyChangedEventArgs(propertyName));
    }
}


Ух! А это только одно свойство! Кстати, вы в курсе? Стажер уволился пару дней назад

Нужно «объединение методов (или других функций), работающих с инкапсулированными данными, без этих данных». Другими словами: внедрить реализацию действий INotifyPropertyChanged не меняя или с минимальными изменениями в классы доменной модели типа Customer.

Если мы можем это реализовать, то на выходе получим:
• Четкое разделение действий
• Избежим повторения кода и ускорим разработку
• Не будем прятать доменные классы под тоннами шаблонного кода

Здорово. Но как всё это сделать?

Теоретический ответ

У нас есть внедряемое действие, которое должно выполняться в нескольких классах (далее по тексту – цели). Реализация (код который реализует протоколирование, кэширование или еще что-то) называется в АОП действие.

Далее нам нужно прикрепить (внедрить, встроить, выберите своё слово) действие (я повторю, потому, что это важно: действие – это реализация внедряемого действия) в любое нужное нам место цели. И мы должны иметь возможность выбрать любое из следующих мест цели для внедрения действия:
• Статические инициализаторы
• Конструкторы
• Чтение и запись статических свойств
• Чтение и запись свойств экземпляров
• Статические методы
• Методы экземпляра
• Деструкторы

В идеальном АОП мы должны иметь возможность внедрить действие в любой строчке кода цели.
Отлично, но если нам нужно прикрепить действие, необходим перехватчик в цели? Да, КЭП!

В АОП описание перехватчика (места, в котором действие будет выполняться) имеет название: срез (pointcut). А место, в котором код фактически привяжется: точка соединения.

Понятно? Возможно нет… Вот немного псевдокода, который, надеюсь, даст пояснения:
// Цель
class BankAccount
{
    public string AccountNumber {get;}
    public int Balance {get; set;}
    void Withdraw(int AmountToWithdraw)
    {
       
:public pointcut1;   // срез (представьте себе, что это что-то вроде метки в Бейсике)
        Balance -= AmountToWithdraw;
    }
}

// Действие протоколирования
concern LoggingConcern
{
    void LogWithdraw(int AmountToWithdraw)
    { 
        // Тут предстаьте себе, что происходит некое волшебство
        // и 'this' – это экземпляр класса BankAccount.
        Console.WriteLine(this.AccountNumber + " withdrawal on-going...");
    }
}

class Program
{
    void Main()
    {
        // получим ссылку на маркер среза через рефлексию
        pointcut = typeof(Bank).GetPointcut("pointcut1");

        // а это точка соединения 
        LoggingConcern.Join(cutpoint,
    LogWithdraw); 

        // После точки соединения среда исполнениея будет иметь запись, 
        // которая требует выполнить LoggingConcern в срезе pointcut1 класса
    }
}

Было бы круто иметь такой механизм в С# прямо «из коробки»???

Еще несколько понятий

До того как мы двинемся далее к нашей реализации, приведем еще несколько понятий…

Что такое Аспект?
Это совокупность действия, среза и точки соединения.

Поразмышляйте над этим минуту, и, я надеюсь, всё встанет на свои места: имеется механизм протоколирования (действие), который регистрируется свой метод протоколирования для исполнения (точка соединения) в указанном месте (срезе) моего приложения. Всё это вместе является аспектом моего приложения.

Но минутку… Что должен и может делать действие после внедрения?

Действия разделяются на две категории:

Побочные эффекты.
Побочный эффект – действие, которое не изменяет действия кода в срезе. Побочный эффект, просто добавляет некую команду для исполнения.
Действие протоколирования – хороший пример побочного эффекта. Когда среда выполняет целевой метод (например, Bank.Withdraw(int Amount)) исполнится метод LoggingConcern.LogWithdraw(int Amount) и метод Bank.Withdraw(int Amount) продолжит своё исполнение.

Советы.
Советы – действия, которые могут изменить ввод/вывод метода.
Действие кэширование – прекрасный пример. Когда выполняется целевой метод (например, CustomerService.GetById(int Id)) исполняется метод CachingConcern.TryGetCustomerById(int Id) и возвращает значение, найденное в кэше, или продолжает исполнение при его отсутствие.

Советам можно:
• Проверять параметры в срезе цели и возможность менять их при необходимости
• Отменять выполнение целевых методов и замещать их другой реализацией
• Проверять возвращаемое значение целевого метода и изменять или замещать их

Вы всё еще читаете? Браво! Parce que vous le valez bien…

На этом заканчиваем с концепций и понятиями АОП. Давайте познакомимся с ним поближе на C#.

Наша реализация


Покажи мне код u je tue le chien!

Действия (concern)

Действие должно реализовывать волшебство this, которое относится к типу нашей цели.
No problemo!
public interface IConcern<T>
{
    T This { get; } // вообще читерство, но кого волнует?
}


Срезы (pointcut)

Нет простого способа получить срезы для каждой строчки кода. Но по одной мы всё-таки можем получить и это довольно просто используя System.Reflection.MethodBase класс. MSDN о нём не многословен: Предоставляет сведения о методах и конструкторах.

Между нами, использование MethodBase для получения ссылок на срезы – наиболее мощное из возможных средств в нашей задаче.
Можно получить доступ к срезам конструкторов, методов, свойств и событий, потому что практически все что вы объявляете в .Net в конечном итоге сводится к методу…

Смотрите сами:
public class Customer 
{
    public event EventHandler<EventArgs> NameChanged;
    public string Name { get; private set; }
        
    public void ChangeName(string newName) 
    {
        Name = newName;
        NameChanged(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var t = typeof(Customer);

        // Конструктор (и неограничваясь пустым)
        var pointcut1 = t.GetConstructor(new Type[] { });

        // Метод ChangeName
        var pointcut2 = t.GetMethod("ChangeName");

        // Свойство Name
        var nameProperty = t.GetProperty("Name");
        var pointcut3 = nameProperty.GetGetMethod();
        var pointcut4 = nameProperty.GetSetMethod();

        // Всё связанное с событием NameChanged
        var NameChangedEvent = t.GetEvent("NameChanged");
        var pointcut5 = NameChangedEvent.GetRaiseMethod();
        var pointcut6 = NameChangedEvent.GetAddMethod();
        var pointcut7 = NameChangedEvent.GetRemoveMethod();
    }
}


Точки соединения (joinpoint)

Писать код для соединения реально просто. Посмотрите на этот код:
void Join(System.Reflection.MethodBase pointcutMethod, System.Reflection.MethodBase concernMethod);

Мы можем добавить его во что-то вроде реестра, который сделаем позже, и можем начинать писать код вроде этого!!!
public class Customer
{
    public string Name { get; set;}
    public void DoYourOwnBusiness() 
    {
        System.Diagnostics.Trace.WriteLine(Name + " занят своим делом");
    }
}

public class LoggingConcern : IConcern<Customer>
{
    public Customer This { get; set; }

    public void DoSomething() 
    {
        System.Diagnostics.Trace.WriteLine(This.Name + " собирается заняться своим делом");

        This.DoYourOwnBusiness();
            
        System.Diagnostics.Trace.WriteLine(This.Name + " закончил заниматься своим делом");
    }
}

class Program
{
    static void Main(string[] args)h
    {
        // получить срез в Customer.DoSomething();
        var pointcut1 = typeof(Customer).GetMethod("DoSomething");
        var concernMethod = typeof(LoggingConcern).GetMethod("DoSomething");

        // Соединить их
        AOP.Registry.Join(pointcut1, concernMethod);
    }
}

Далеко ли мы ушли от нашего псевдокода? На мой взгляд, не очень…
Так что дальше?

Собираем всё вместе…

Вот тут одновременно начинаются проблемы и веселье!
Но начнем с простого

Реестр

Реестр будет хранить записи о наших точках соединения. Берем список-синглтон для точек соединения. Точка соединения – простая структура:
public struct Joinpoint
{
    internal MethodBase PointcutMethod;
    internal MethodBase ConcernMethod;
    
    private Joinpoint(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        PointcutMethod = pointcutMethod;
        ConcernMethod = concernMethod;
    }

    // служебный метод для создания точек соединения 
    public static Joinpoint Create(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        return new Joinpoint (pointcutMethod, concernMethod);
    }
}

Ничего особенного… Еще ему нужно реализовывать IEquatable, но, чтобы сделать код короче, я его убрал.

И реестр. Класс называется AOP и является синглтоном. Он предоставляет доступ к своему уникальному экземпляру через статическое свойство названое Registry:
public class AOP : List<Joinpoint>
{
    static readonly AOP _registry;
    static AOP() { _registry = new AOP(); }
    private AOP() { }
    public static AOP Registry { get { return _registry; } }

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Join(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        var joinPoint = Joinpoint.Create(pointcutMethod, concernMethod);
        if (!this.Contains(joinPoint)) this.Add(joinPoint);
    }
}


С помощью класса AOP можно написать такую конструкцию:
AOP.Registry.Join(pointcut, concernMethod);


Хьюстон, у нас проблемы

Здесь мы столкнулись с очевидной и большой проблемой, с которой надо что-то делать. Если разработчик напишет вот так
var customer = new Customer {Name="test"};
customer.DoYourOwnBusiness();

то нет причин, по которым нужно обращаться к нашему реестру, и наш метод LoggingConcern.DoSomething() не запустится.

Беда в том, что .Net не предоставляет нам простого способа перехватить такие вызовы.
Раз нет встроенного механизма, нужно сделать свой. Возможности вашего механизма будут определять возможности вашей реализации АОП.
Цель данной статьи – не обсуждение всех возможных техник перехвата. Просто обратите внимание, что способ перехвата является ключевым отличием реализаций АОП.

Сайт SharpCrafters (владельцы PostSharp) приводят некоторую четкую информацию по двум основным техникам:
• Встраивание на этапе компиляции
• Встраивание во время исполнения

Наша реализация – Прокси

В общем, не секрет, что для осуществления перехвата есть три варианта:
• Создать свой язык и компилятор для получения сборок .net: при компиляции можно внедрить что угодно куда угодно.
• Реализовать решение, изменяющее поведение сборок при исполнении.
• Поставить между клиентом и целью прокси, осуществляющее перехват вызовов.

Замечание для продвинутых парней: я нарочно не рассматриваю возможности API отладчика и профилировщика, так как их использование нежизнеспособно в продакшене.

Замечание для самых продвинутых: гибрид первого и второго варианта, используемый в Raslyn API, может быть реализован, но, как я знаю, пока ещё только делается. A bon entendeur…

Тем более, если вам не нужно иметь возможность делать срезы в любой строчке кода, первые два варианта чересчур сложны.

Перейдем к третьему варианту. Есть две новости по поводу прокси: хорошая и плохая.
Плохая – во время исполнения нужно подменять цель на экземпляр прокладки. Если хочется перехватывать конструкторы, то придется делегировать создание экземпляров целевых классов фабрике, такое встраивание действий эта реализация не может. Если существует экземпляр целевого класса необходимо явно запросить замену. Для мастеров инверсии управления и внедрения зависимостей – это даже не задача. Для остальных же это значит, что придется использовать фабрику для обеспечения всех возможностей в нашей технике перехвата. Но не волнуйтесь, мы построим эту фабрику.

Хорошая новость – для реализации прокси ничего делать не надо. Класс System.Runtime.Remoting.Proxies.RealProxy построит его оптимальным способом.
По-моему, название класса не отражает его назначения. Этот класс – не прокси, а перехватчик. Тем не менее, он сделает нам прокси вызовом его метода GetTransparentProxy(), а это то, что нам, собственно, от него и нужно.

Вот рыба перехватчика:
public class Interceptor : RealProxy, IRemotingTypeInfo
{
    object theTarget { get; set; }

    public Interceptor(object target) : base(typeof(MarshalByRefObject))
    {
        theTarget = target;
    }

    public override System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg)
    {
        IMethodCallMessage methodMessage = (IMethodCallMessage) msg;
        MethodBase method = methodMessage.MethodBase;
        object[] arguments = methodMessage.Args;

        object returnValue = null;

        // TODO:
        // реализация подмены метода для случая, когда AOP.Registry 
        // уже содержит точку соединения MethodBase, содержащуюся в переменной"method"...
        // если в реестре нет точки соединения, просто искать соответствующий метод 
        // в объекте "theTarget" и вызвать его... ;-)

        return new ReturnMessage(returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage);
    }

    #region IRemotingTypeInfo
    public string TypeName { get; set; }
    public bool CanCastTo(Type fromType, object o) { return true; }
    #endregion
}

Некоторые пояснения, т.к. мы забрались в самое сердце реализации…

Класс RealProxy создан для перехвата вызовов от удалённых объектов и упорядочиванию целевых объектов. Под удалёнными следует понимать по-настоящему удаленные: объекты из другого приложения, другого домена приложений, другого сервера и т.д.). Не углубляясь в детали, имеется два способа упорядочивания удалённых объектов в инфраструктуре .Net: по ссылке и по значению. Поэтому упорядочить удалённые объекты можно только, если они наследуют MarshalByRef или реализуют ISerializable. Мы не собираемся использовать возможности удаленных объектов, но тем не менее нам необходимо, чтобы класс RealProxy думал, что цель поддерживает удаленное управление. Из-за этого мы передаем typeof(MarshalByRef) в конструктор RealProxy.

RealProxy получает все вызовы через прозрачный прокси с помощью метода Invoke(System.Runtime.Remoting.Messaging.IMessage msg). Именно здесь мы реализуем суть подмены методов. Смотри комментарии в коде выше.

Касательно реализации IRemotingTypeInfo: в реальной удаленной среде, клиент запросит у сервера объект. Клиентское приложение может вообще ничего не знать о типе получаемого объекта. Соответственно, когда клиентское приложение вызывает public object GetTransparentProxy() среда может ли возвращаемый объект (прозрачный прокси) соответствовать контракту приложения. Реализуя IRemotingTypeInfo мы даем подсказку среде клиента какое приведение допустимо, а какое нет.

А теперь дивитесь, какой трюк мы здесь используем.
public bool CanCastTo(Type fromType, object o) { return true; }    

Вся наша реализация АОП возможна исключительно благодаря возможности написать в для удаленного объекта эти два слова: return true. Которые означают, что что мы можем привести объект возвращаемый GetTransparentProxy() к какому угодно интерфейсу без какой-либо проверки средой!!!!

Среда просто дает нам добро на любые действия!
Можно поправить этот код и возвращать что-то более разумное, чем true для любого типа… Но можно также, представить себе как извлечь пользу из поведения предоставляемым Несуществующим Метода или перехватить весь интерфейс… В общем, появляется довольно много пространства для фантазии…

Сейчас у нас уже есть достойный механизм перехвата для нашего целевого экземпляра. Но у нас всё еще нет перехвата конструкторов и прозрачного прокси. Это задача для фабрики…

Фабрика

Сказать особо нечего. Вот рыба класса.
public static class Factory
{
    public static object Create<T>(params object[] constructorArgs)
    {
        T target;

        // TODO:
        // Основываясь на typeof(T) и списке constructorArgs (кол-ву и их типах)
        // мы можем спросить реестр, есть ли в нём точка соединения для конструктора
        // и вызвать его, а если нет - найти соответствующий и вызвать
        // Присвоить результат конструктора переменной “target” (цель) и передать
        // её методу GetProxy
        return GetProxyFor<T>(target);
    }

    public static object GetProxyFor<T>(object target = null)
    {
        // Здесь мы перехватываем вызовы к существующему экземпляру объекта
        // (может мы его и создали, но необязательно)
        // Просто создайте перехватчик и верните прозрачный прокси
        return new Interceptor(target).GetTransparentProxy();
    }
}

Обратите внимание, что класс Factory всегда возвращает объект типа объект. Мы не можем вернуть объект типа Т просто потому, что прозрачный прокси не типа Т, он типа System.Runtime.Remoting.Proxies.__TransparentProxy. Но помните о разрешении данном нам средой, мы можем привести возвращаемый объект к любому интерфейсу без проверки!

Поселим класс Factory в наш класс AOP с надеждой, что мы передадим нашим заказчикам опрятный код. На это можно посмотреть в разделе Использование.

Последние замечания по реализации

Если вы дочитали до этой точки, то вы – герой! Bravissimo! Kudos!

Чтобы сохранить статью краткой и понятной (и чего смешного?), я не стану вдаваться в скучные рассуждения о деталях реализации получения и переключения методов. В этом нет ничего интересного. Но если вам всё-таки интересно: качайте код и смотрите – он полностью рабочий! Названия классов и методов, могут немного отличаться, т.к. я правил его параллельно, но особых изменений быть не должно.

Внимание! Перед использованием кода в вашем проекте, внимательно прочитайте paenultimus. А если не знаете что значит paenultimus, кликайте по ссылке.


Использование

Я уже много чего написал, но до сих пор не показал, что же со всем этим делать. И вот он момент истины!

Что в архиве (качать)

Архив включает в себя 5 примеров, демонстрирующих внедрение следующих аспектов:
• Перехват конструктора
• Перехват методов и свойств
• Перехват событий
• Перехват инициализации типов
• Перехват File.ReadAllText(string path)

А здесь я продемонстрирую два из пяти: наиболее и наименее очевидный.

Перехват методов и свойств

Первое, нам нужна доменная модель. Ничего особенного.
public interface IActor
{
    string Name { get; set; }
    void Act();
}

public class Actor : IActor
{
    public string Name { get; set; }

    public void Act()
    {
        Console.WriteLine("My name is '{0}'. I am such a good actor!", Name);
    }
}


Теперь нам нужно действие
public class TheConcern : IConcern<Actor>
{
    public Actor This { get; set; }

    public string Name 
    {
        set
        {
            This.Name = value + ". Hi, " + value + " you've been hacked";
        }
    }

    public void Act()
    {
        This.Act();
        Console.WriteLine("You think so...!");
    }
}


При инициализации приложения мы сообщаем реестру о наших точках соединения
// Weave the Name property setter
AOP.Registry.Join
    (
        typeof(Actor).GetProperty("Name").GetSetMethod(),
        typeof(TheConcern).GetProperty("Name").GetSetMethod()
    );

// Weave the Act method
AOP.Registry.Join
    (
        typeof(Actor).GetMethod("Act"),
        typeof(TheConcern).GetMethod("Act")
    );   


И, наконец, мы создаем объект в фабрике
var actor1 = (IActor) AOP.Factory.Create<Actor>();
actor1.Name = "the Dude";
actor1.Act();

Обратите внимание, что мы запросили создание класса Actor, но мы можем привести результат к интерфейсу, потому будем приводить к IActor, т.к. класс его реализует.

Если запустить всё это в консольном приложении, получим:
My name is 'the Dude. Hi, the Dude you've been hacked'. I am such a good actor!
You think so...!


Перехват File.ReadAllText(string path)

Здесь две небольшие проблемы:
• Класс File статичный
• и не реализует никакие интерфейсы

Помните о «добро»? Среда не проверяет тип возвращаемый прокси и соответствие интерфейсу.
Значит мы можем создать любой интерфейс. Никто его не реализует ни цель, ни действие. В общем, мы используем интерфейс как контракт.
Давайте создадим интерфейс прикидывающийся статичным классом File.
public interface IFile
{
    string[] ReadAllLines(string path);
}


Наше действие
public class TheConcern
{
    public static string[] ReadAllLines(string path)
    {
        return File.ReadAllLines(path).Select(x => x + " hacked...").ToArray();
    }
}


Регистрация точек соединения
AOP.Registry.Join
    (
        typeof(File).GetMethods().Where(x => x.Name == "ReadAllLines" && x.GetParameters().Count() == 1).First(),
        typeof(TheConcern).GetMethod("ReadAllLines")
    );


И, наконец, исполнение программы
var path = Path.Combine(Environment.CurrentDirectory, "Examples", "data.txt");
var file = (IFile) AOP.Factory.Create(typeof(File));
foreach (string s in file.ReadAllLines(path)) Console.WriteLine(s);


В этом примере, обратите внимание, что мы не можем использовать метод Factory.Create, т.к. статические типы не могут использоваться как аргументы.

Ссылки


Без какого-либо особого порядка.
Tutorials from Qi4j
Extending RealProxy
Dynamic Proxy Tutorial (Castle.net)
LinFu.DynamicProxy: A Lightweight Proxy Generator
Add aspects using Dynamic Decorator
Implementing a CLR Profiler to act as an AOP interceptor
Intercepting the .Net
AspectF Fluent Way to Add Aspects for Cleaner Maintainable Code

Что дальше?


Мы смогли добиться основной цели АОП: реализовать аспект и зарегистрировать его для исполнения. TinyAOP появился на свет. Но ваш путь в землях АОП ещё не закончен и, возможно, вы захотите углубиться.

Причина 1: Кому охота регистрировать точки соединения так, как мы это делаем сейчас? Точно не мне! Немного анализа и можно сделать всё более практичным и похожим на настоящую АОП-библиотеку. АОП нужно для упрощения жизни, а не создания головной боли.

Причина 2: Темы примесей и агрегирования вообще не раскрыты, а от них можно ожидать много чего хорошего.

Причина 3: Нам нужна производительность и стабильность. Сейчас код всего лишь доказательство работоспособности концепции. Он довольно медленный и его можно сделать очень быстрым. Проверка ошибок тоже не помешает.

Причина 4: Мы перехватываем почти все классы, но как быть с перехватом интерфейсов?

Причина 5: Вам еще мало причин?

Заключение


У нас есть хороший и компактный прототип, демонстрирующий техническую возможность реализации АОП в чистом управляемом коде без вшивания и т.п.

Теперь вы знаете об АОП, можете написать собственную реализацию.

Тут внедряется переводчик и начинает нести отсебятину


Хотя и полностью согласен с автором по поводу этого раздела.
Сайты вроде habahabr.ru, codeproject.com и им подобные существуют только потому, что разные люди публикуют на них статьи. Неважно почему они это делают, но эта работа делается. Пожалуйста, не пренебрегайте авторами. Если вам не понравилась статья, объясните в комментариях, что не так. Если вы будете просто минусовать, никто не узнает, как сделать лучше. Если понравилась, тоже не молчите!

А теперь точно отсебятина переводчика


Вообще, к моему удивлению вменяемо перевести многие слова из АОП не получилось, потому предлагаю в комментах предложить хорошие и понятные русские слова для следующих терминов:
• Aspect (тут вроде понятно, но всё же)
• Concern
• Joinpoint
• Cross-cutting
• Weaving (вивер — ничего, но лучше бы русское слово найти)

Автор: Guirec Le Bars
Tags:
Hubs:
+33
Comments 8
Comments Comments 8

Articles