В моем разговоре с Андресом Хейлсбергом о страхах, неуверенности и сомнений относительно Аспектно-Ориентированного программирования я упомянул про общую путаницу и непонимание что АОП и DI – это конкурирующие концепции. В этой статье я попробую объяснить вам обо всех различиях и сходствах в этих двух подходах.Недавно я прекрасно провел время за чтением статьи Дино Эспосито: «Аспектно-ориентированное программирование, interception и unity 2.0», в декабрьском номере MSDN Magazine. Это великолепная статья и я настоятельно советую любому разработчику, вовлеченному в .NET разработку прочитать ее полностью. Как и многие DI фреймворки, и некоторые основные фреймворки (WCF, ASP.NET MVC), Unity предлагает похожую на АОП возможность: interceptors.
Dependency Injection – архитектурный шаблон, который предназначен для уменьшения зависимостей между компонентами. Одним из преимуществ снижения зависимостей (перехода от тесно-связанной архитектуры к слабо связанной) является сокращение уровня информированности компонентов друг о друге. Тем самым достигается лучшее разделение задач, используя принцип проектирования «программа, основанная на интерфейсе, а не на классах» (ориг.: «program to an interface, not to an implementation») Общее неправильное толкование этого шаблона проектирования среди разработчиков C# и Java состоит в том что клиенты обычно работают с интерфейсами (то, что вы объявляете ключевым словом interface), а не с классами. Я предпочитаю трактовать таким образом: «программа-на-контракт, не программа-на-реализацию» (ориг.: “program to a contract, not to an implementation”). Контракт на использование класса в данном случае состоит из открытых (public) членов класса, снабженных документацией (плюс их пред-, постусловия, если это может быть применимо). Чтобы быть еще более точным: «программа, основанная на документации, не на классах». Таким образом, если ваша цель – всего лишь лучше разделить понятия, вам интерфейсы в действительности не так сильно и нужны. Вам необходимы всего лишь ключевые слова public, Internal, protected и private. Важно отметить, что в объектно-ориентированном дизайне интерфейсы вовсе не догма. Далеко не все разработчики фреймворков придерживаются этой философии: «библиотека классов Java основана на интерфейсах в большей степени чем .NET, просто потому что .NET разработчики имеют негативный опыт использования интерфейсов COM»
Программирование, в основе которого лежат интерфейсы (не классы) полезно, когда ваш компонент должен работать со многими реализациями интерфейса. Есть бесчисленное множество случаев, когда это будет очень полезно (в конце концов, все мы хотим, чтобы наша архитектура была расширяемой), но давайте посмотрим правде в глаза: во многих случаях, когда наш компонент должен взаимодействовать с другим компонентом, мы прекрасно знаем его последующую реализацию, и мы прекрасно знаем что другой реализации не предвидится. Так зачем же тогда его (интерфейс) здесь использовать? Существует два варианта, когда вы можете захотеть использовать концепцию программы, основанной на интерфейсах:
В этих ситуациях (которые на самом деле случаются только когда дело доходит до тестирования) на самом деле очень хорошо, что компоненты не связаны в исходном коде, даже если мы знаем, каким образом они будут зависеть друг от друга при окончательной установке. Итак, как же мы свяжем эти компоненты вместе? И тут в игру вступает Dependency Injection. С шаблоном проектирования Dependency Injection, вы инструктируете «контейнер» как объекты должны быть связаны друг с другом, и вы заставляете этот «контейнер» связывать объекты друг с другом. В действительности, вы не создаете объекты классов, используя конструкторы, вы делаете это, используя методы «контейнера». Контейнер принимает решение, какая реализация вам необходима, создает объект и возвращает его вам.
Dependency Injection решает проблему связывания слабо-связанных компонент друг с другом. Аспектно-Ориентированное программирование решает самые различные проблемы. Ведь в реальности, где различные функции и возможности (такие как аудит, логгирование, обработка транзакций) влияют практически на все бизнес-требования, АОП выражает эту сложную реальность в виде компактного решения, с минимальным дублированием кода. Когда мы хотим провести code review, провести аудит нашего кода, нашим естественным желанием будет посмотреть только один файл. Однако когда мы делаем ревизию кода, который относится к неким management требованиям заказчика, мы не хотим видеть этот код. Как вы понимаете, этого не возможно достичь, используя лишь объектно-ориентированное программирование, и это послужило причиной возникновения и развития Аспектно-Ориентированного программирования.
Существуют некоторые сходства между DI и АОП:
Тем не менее, DI и АОП значительно отличаются в ситуациях, когда они оказываются полезными:
Так все-таки, каким образом Dependency Injection стала ассоциироваться с Аспектно-Ориентированным Программированием? Да просто шаблон проектировани DI позволяет легко добавлять новые поведения компонентам. Вот как это делается: когда вы запрашиваете зависимость от контейнера, вы ожидаете получить реализацию интерфейса. Вы только можете предполагать, какую реализацию вы получите, но зачастую, вы играете с ним в игру. Если вы попросите контейнер добавить некоторое поведение (например, трассировку вызовов) к компоненту, вы не получите сам объект. Вместо этого вы получите динамический прокси-объект. Прокси-объект будет находиться между вам и запрошенным компонентом, и будет реализовывать то поведение, которое вы запросили. И, поскольку, DI фреймворки имеют возможность вставлять поведения между клиентом и реализацией компоненты, DI понимается многими как одна из технологий, которая может давать вам возможность разрабатывать в рамках АОП.
Что я люблю в proxy-based подходе, так это то, как АОП вписывается в концепцию DI. DI помогает вам настраивать контейнер, каким образом он будет строить компоненты, используя зависимости между объектами, устанавливая их свойства и вызывая инициализирующие методы. Вы можете сконфигурировать аспекты просто как некоторые зависимости в XML файле или используя код на C#, т.о. построение и сборка компонентов будут унифицированы.
Возможность добавлять аспекты, всего лишь изменяя конфигурационные файлы, может оказаться очень полезной возможностью. Предположим, что где-то есть какая-то проблема. И вы хотите отслеживать все обращения к какому-то компоненту. Вы сможете сделать это, не перекомпилируя все приложение. Это может стать решающим преимуществом в среде выполнения, где нет соответствующих средств для отладки. Где для того, чтобы найти проблему и решить ее, может пройти несколько месяцев.
Proxy-based АОП, которое реализовано при помощи DI фреймворков имеет большое количество ограничений:
Они
Когда разработчик начинает что-то практиковать постоянно, не отвлекаясь на конкурирующие технологии, это становится риском, потому что человек, практикующий что-то постоянно, чувствует себя безопасно. И практика становится догмой. Это случается в TDD, которые навязывают людям писать тесты на бессмысленном уровне детализации. И тоже самое, с Dependency Injection, где люди навязывают изолировать все классы друг от друга. Запомните, что DI изначально была придумана, чтобы изолировать компоненты, а не классы. Компоненты это части приложения, его блоки, не объекты. Я обеспокоен что мы вынуждены (под давлением общества) писать намного более сложные приложения только для того чтобы удовлетворить требования к тестированию, которые на самом деле, не приводят к улучшению качества конечного продукта. Необходимо запомнить, что unit-тестирование – это инструмент, а не цель. Конечной целью является качественная работа вашего приложения на протяжении всей его жизни.
Аспектно-ориентированное программирование и Dependency Injection – очень разные концепции, но есть ограниченное количество случаев, когда они хорошо подходят друг к другу. В этих ситуациях, использование АОП в рамках DI имеет смысл. Для других вариантов вы имеете только два выхода: злоуптреблять dynamic proxy (и это не решение), либо использовать специализированный инструмент для АОП разработки. И эта дилемма не только для меня, PostSharp, или .NET. Такая же дилемма висит и над Java программистами, которые думают, что выбрать: Spring AOP или AspectJ. АОП и DI не являются конкурирующими технологиями. Вы можете использовать их вместе в одном проекте. Однако, что важно знать и понимать, для чего каждая технология была сделана и сфокусироваться на качестве вашего кода.
Ссылки:
Что такое Dependency Injection?
Dependency Injection – архитектурный шаблон, который предназначен для уменьшения зависимостей между компонентами. Одним из преимуществ снижения зависимостей (перехода от тесно-связанной архитектуры к слабо связанной) является сокращение уровня информированности компонентов друг о друге. Тем самым достигается лучшее разделение задач, используя принцип проектирования «программа, основанная на интерфейсе, а не на классах» (ориг.: «program to an interface, not to an implementation») Общее неправильное толкование этого шаблона проектирования среди разработчиков C# и Java состоит в том что клиенты обычно работают с интерфейсами (то, что вы объявляете ключевым словом interface), а не с классами. Я предпочитаю трактовать таким образом: «программа-на-контракт, не программа-на-реализацию» (ориг.: “program to a contract, not to an implementation”). Контракт на использование класса в данном случае состоит из открытых (public) членов класса, снабженных документацией (плюс их пред-, постусловия, если это может быть применимо). Чтобы быть еще более точным: «программа, основанная на документации, не на классах». Таким образом, если ваша цель – всего лишь лучше разделить понятия, вам интерфейсы в действительности не так сильно и нужны. Вам необходимы всего лишь ключевые слова public, Internal, protected и private. Важно отметить, что в объектно-ориентированном дизайне интерфейсы вовсе не догма. Далеко не все разработчики фреймворков придерживаются этой философии: «библиотека классов Java основана на интерфейсах в большей степени чем .NET, просто потому что .NET разработчики имеют негативный опыт использования интерфейсов COM»
Программирование, в основе которого лежат интерфейсы (не классы) полезно, когда ваш компонент должен работать со многими реализациями интерфейса. Есть бесчисленное множество случаев, когда это будет очень полезно (в конце концов, все мы хотим, чтобы наша архитектура была расширяемой), но давайте посмотрим правде в глаза: во многих случаях, когда наш компонент должен взаимодействовать с другим компонентом, мы прекрасно знаем его последующую реализацию, и мы прекрасно знаем что другой реализации не предвидится. Так зачем же тогда его (интерфейс) здесь использовать? Существует два варианта, когда вы можете захотеть использовать концепцию программы, основанной на интерфейсах:
- Вы зависите от компонента, который не был вам предоставлен, но у вас есть потребность в начале разработки, вы не можете ждать. И что еще более важно, вам уже сейчас надо тестировать ваш собственный код. Вам необходимо симулировать (mock) работу этого компонента, пока он не будет вам предоставлен, только в целях тестирования.
- Вы зависите от компонента, который имеет постоянное состояние (например, база данных), какой-то процесс реального мира (например, система обработки банковской карты) или просто существует зависимость от медленного либо ненадежного ресурса (вэб-сервис). Вам необходимо симулировать работу этих компонент с вашей собственной реализацией только в целях тестирования.
В этих ситуациях (которые на самом деле случаются только когда дело доходит до тестирования) на самом деле очень хорошо, что компоненты не связаны в исходном коде, даже если мы знаем, каким образом они будут зависеть друг от друга при окончательной установке. Итак, как же мы свяжем эти компоненты вместе? И тут в игру вступает Dependency Injection. С шаблоном проектирования Dependency Injection, вы инструктируете «контейнер» как объекты должны быть связаны друг с другом, и вы заставляете этот «контейнер» связывать объекты друг с другом. В действительности, вы не создаете объекты классов, используя конструкторы, вы делаете это, используя методы «контейнера». Контейнер принимает решение, какая реализация вам необходима, создает объект и возвращает его вам.
Аспектно-ориентированное программирование
Dependency Injection решает проблему связывания слабо-связанных компонент друг с другом. Аспектно-Ориентированное программирование решает самые различные проблемы. Ведь в реальности, где различные функции и возможности (такие как аудит, логгирование, обработка транзакций) влияют практически на все бизнес-требования, АОП выражает эту сложную реальность в виде компактного решения, с минимальным дублированием кода. Когда мы хотим провести code review, провести аудит нашего кода, нашим естественным желанием будет посмотреть только один файл. Однако когда мы делаем ревизию кода, который относится к неким management требованиям заказчика, мы не хотим видеть этот код. Как вы понимаете, этого не возможно достичь, используя лишь объектно-ориентированное программирование, и это послужило причиной возникновения и развития Аспектно-Ориентированного программирования.
Сходства и различия между АОП и DI
Существуют некоторые сходства между DI и АОП:
- Оба добиваются слабой связанности в архитектуре
- Оба добиваются лучшего разделения функционала
- Оба разгружают часть проблем с основного кода
Тем не менее, DI и АОП значительно отличаются в ситуациях, когда они оказываются полезными:
- DI хорош, когда вы имеете зависимость от компонент, причем вам не важно как они реализованы;
- АОП хорош, когда вам необходимо применить какое-то поведение к большому количеству элементов кода. Причем, целевой код не обязательно зависит от этого поведения
Dynamic Proxies
Так все-таки, каким образом Dependency Injection стала ассоциироваться с Аспектно-Ориентированным Программированием? Да просто шаблон проектировани DI позволяет легко добавлять новые поведения компонентам. Вот как это делается: когда вы запрашиваете зависимость от контейнера, вы ожидаете получить реализацию интерфейса. Вы только можете предполагать, какую реализацию вы получите, но зачастую, вы играете с ним в игру. Если вы попросите контейнер добавить некоторое поведение (например, трассировку вызовов) к компоненту, вы не получите сам объект. Вместо этого вы получите динамический прокси-объект. Прокси-объект будет находиться между вам и запрошенным компонентом, и будет реализовывать то поведение, которое вы запросили. И, поскольку, DI фреймворки имеют возможность вставлять поведения между клиентом и реализацией компоненты, DI понимается многими как одна из технологий, которая может давать вам возможность разрабатывать в рамках АОП.
Proxy-based АОП: что хорошего?
Что я люблю в proxy-based подходе, так это то, как АОП вписывается в концепцию DI. DI помогает вам настраивать контейнер, каким образом он будет строить компоненты, используя зависимости между объектами, устанавливая их свойства и вызывая инициализирующие методы. Вы можете сконфигурировать аспекты просто как некоторые зависимости в XML файле или используя код на C#, т.о. построение и сборка компонентов будут унифицированы.
Возможность добавлять аспекты, всего лишь изменяя конфигурационные файлы, может оказаться очень полезной возможностью. Предположим, что где-то есть какая-то проблема. И вы хотите отслеживать все обращения к какому-то компоненту. Вы сможете сделать это, не перекомпилируя все приложение. Это может стать решающим преимуществом в среде выполнения, где нет соответствующих средств для отладки. Где для того, чтобы найти проблему и решить ее, может пройти несколько месяцев.
Proxy-based АОП: что плохого?
Proxy-based АОП, которое реализовано при помощи DI фреймворков имеет большое количество ограничений:
Они
- работают только на компонентах, реализованных через интерфейс. Они не работают на всех методах, которые являются внутренними для компонента, будь то public или private, static или instance. Запомните что компонента – часть приложения, и, как правило, включает в себя множество классов;
- Они будут работать только в тех случаях, когда клиентский код получит ссылку на компонент из контейнера. Аспекты не будут применяться, если компонент будет вызывать собственные методы, либо компонент будет вызываться без непосредственного взаимодействия через контейнер;
- Т.к. аспекты применяются во время выполнения, нет никакой возможности сделать проверки во время компиляции. Build-time фреймворки, такие как PostSharp, предоставят вам ошибки не правильного использования аспектов во время компиляции и даже могут предоставить разработчику аспекта различные пути сделать это правильно (например, нельзя добавлять кэширующий аспект к методам, которые возвращают Stream, или к методам, которые имеют аспекты, связанные с безопасностью);
- Трудно понять (а) какие аспекты применились к частям кода, с которыми вы работаете, и (б) к каким частям кода, с которыми вы работаете, были применены аспекты. Полные фреймворки, такие как PostSharp или AspectJ, могут высветить вам полную информацию, прямо в IDE. Что помогает значительно улучшить понимание исходного кода.
- Run-time АОП имеет гораздо худшую производительность, чем build-time фреймворки. Это может иметь второстепенное значение, если вы применяете аспекты к малому количеству «грубо»-спроектированных классов. Однако, если вы будете создавать динамические прокси к сотням классов, вы почувствуете значительные расходы на производительность;
- Динамические прокси были всегда только сторонней особенностью DI фреймворков и никогда не реализовывали полного видения, даже если это технически осуществимо. Они всегда ограничены на внедрении в методы, и это невозможно сравнить с тем, что могут предложить PostSharp и AspectJ.
Proxy-based АОП: что уродливо?
Когда разработчик начинает что-то практиковать постоянно, не отвлекаясь на конкурирующие технологии, это становится риском, потому что человек, практикующий что-то постоянно, чувствует себя безопасно. И практика становится догмой. Это случается в TDD, которые навязывают людям писать тесты на бессмысленном уровне детализации. И тоже самое, с Dependency Injection, где люди навязывают изолировать все классы друг от друга. Запомните, что DI изначально была придумана, чтобы изолировать компоненты, а не классы. Компоненты это части приложения, его блоки, не объекты. Я обеспокоен что мы вынуждены (под давлением общества) писать намного более сложные приложения только для того чтобы удовлетворить требования к тестированию, которые на самом деле, не приводят к улучшению качества конечного продукта. Необходимо запомнить, что unit-тестирование – это инструмент, а не цель. Конечной целью является качественная работа вашего приложения на протяжении всей его жизни.
Заключение
Аспектно-ориентированное программирование и Dependency Injection – очень разные концепции, но есть ограниченное количество случаев, когда они хорошо подходят друг к другу. В этих ситуациях, использование АОП в рамках DI имеет смысл. Для других вариантов вы имеете только два выхода: злоуптреблять dynamic proxy (и это не решение), либо использовать специализированный инструмент для АОП разработки. И эта дилемма не только для меня, PostSharp, или .NET. Такая же дилемма висит и над Java программистами, которые думают, что выбрать: Spring AOP или AspectJ. АОП и DI не являются конкурирующими технологиями. Вы можете использовать их вместе в одном проекте. Однако, что важно знать и понимать, для чего каждая технология была сделана и сфокусироваться на качестве вашего кода.
Ссылки: