лекция. Сборник лекций по МДК _Технология разработки программного обеспе. Курс лекций для специальности спо базовой подготовки
Скачать 4.41 Mb.
|
ВводнаяНекоторые могут подумать, что их путь познания Объектно-Ориентированного Программирования ещё не завершён, и есть ли смысл переключаться с ООП на АОП, отказываясь от всех практик изученных за годы тяжкого обучения? Ответ прост: не надо переключаться. Нет противостояния ООП и АОП! АОП – концепция, чьё название, по-моему, вводит в заблуждения. Понимание принципов АОП требует от вас работы с классами, объектами, наследованием, полиморфизмом, абстракциями и т.п., потому у вас никак не получится потерять всё то, чем вы пользуетесь в ООП. В стародавние времена, когда интернет состоял из желтых страниц, досок объявлений и юзнета, лучше всего было читать книжки, чтобы изучить что-то (ремарка для нового поколения: книга – штука, состоящая из сшитых вместе бумажных листов, содержащих текст). И практически все эти книги постоянно напоминали вам о следующих правилах ООП: • Правило №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) и возвращает значение, найденное в кэше, или продолжает исполнение при его отсутствие. Советам можно: • Проверять параметры в срезе цели и возможность менять их при необходимости • Отменять выполнение целевых методов и замещать их другой реализацией • Проверять возвращаемое значение целевого метода и изменять или замещать их На этом заканчиваем с концепций и понятиями АОП. Давайте познакомимся с ним поближе на C#. Наша реализацияПокажи мне код u je tue le chien!Действия (concern)Действие должно реализовывать волшебство this, которое относится к типу нашей цели. public interface IConcern<T> { T This { get; } // вообще читерство, но кого волнует? } Срезы (pointcut)Нет простого способа получить срезы для каждой строчки кода. Но по одной мы всё-таки можем получить и это довольно просто используя System.Reflection.MethodBase класс. MSDN о нём не многословен: Предоставляет сведения о методах и конструкторах. Между нами, использование MethodBase для получения ссылок на срезы – наиболее мощное из возможных средств в нашей задаче. Можно получить доступ к срезам конструкторов, методов, свойств и событий, потому что практически все что вы объявляете в .Net в конечном итоге сводится к методу… Смотрите сами: public class Customer { public event EventHandler 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 target; // TODO: // Основываясь на typeof(T) и списке constructorArgs (кол-ву и их типах) // мы можем спросить реестр, есть ли в нём точка соединения для конструктора // и вызвать его, а если нет - найти соответствующий и вызвать // Присвоить результат конструктора переменной “target” (цель) и передать // её методу GetProxy return GetProxyFor } public static object GetProxyFor { // Здесь мы перехватываем вызовы к существующему экземпляру объекта // (может мы его и создали, но необязательно) // Просто создайте перехватчик и верните прозрачный прокси return new Interceptor(target).GetTransparentProxy(); } } Обратите внимание, что класс Factory всегда возвращает объект типа объект. Мы не можем вернуть объект типа Т просто потому, что прозрачный прокси не типа Т, он типа System.Runtime.Remoting.Proxies.__TransparentProxy. Но помните о разрешении данном нам средой, мы можем привести возвращаемый объект к любому интерфейсу без проверки! Поселим класс Factory в наш класс AOP с надеждой, что мы передадим нашим заказчикам опрятный код. На это можно посмотреть в разделе Использование. Последние замечания по реализацииЕсли вы дочитали до этой точки, то вы – герой! Bravissimo! Kudos! Чтобы сохранить статью краткой и понятной (и чего смешного?), я не стану вдаваться в скучные рассуждения о деталях реализации получения и переключения методов. В этом нет ничего интересного. Но если вам всё-таки интересно: качайте код и смотрите – он полностью рабочий! Названия классов и методов, могут немного отличаться, т.к. я правил его параллельно, но особых изменений быть не должно. Внимание! Перед использованием кода в вашем проекте, внимательно прочитайте paenultimus. А если не знаете что значит paenultimus, кликайте по ссылке. ИспользованиеПерехват методов и свойствПервое, нам нужна доменная модель. Ничего особенного. 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 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, т.к. статические типы не могут использоваться как аргументы. Основы унифицированного процесса разработки |