Хочу поделиться с вами своим опытом изучения парадигмы АОП и разработки с её использованием в крупном проекте.
Аспектно-ориентированное программирование, или АОП, — парадигма, которая выделяет сквозной функционал и изолирует его в виде так называемого аспекта, или аспектного класса. Подразумевается наличие семантических инструментов и механизмов «подкапотной» инъекции аспекта в код приложения. Таким образом, получается, что аспект сам определяет, какие участки приложения ему нужно обрабатывать, в то время как приложение и не догадывается (до компиляции, конечно), что в его участки нагло и бессовестно вводят чужеродный код.
Допустим, что у нас есть довольно тривиальная задача — обеспечить приложению поддержку некоторых языков (russian, english, italian, french, etc.). Вы скажете, что у нас есть языковая и региональная дифференциация всех ресурсов, и будете правы. За исключением случая, когда приложение пользуется не встроенными ресурсами, а «тянет» их с сервера. В общем-то, такая ситуация встречается часто и решается тривиально — добавляем в абстрактный класс BaseActivity, который у нас наследуется от системного, пару строчек на обработчик, и всё работает. А можно обойтись и без этих пары строчек. И даже без базового класса. А при необходимости — просто скопировать в приложение один файл или добавить зависимость в gradle, которая всё сделает сама.
Итак, задача ясна, пишем.
package com.archinamon.example.xpoint; import android.support.v7.app.AppCompatActivity; import android.app.Application; public aspect LocaleMonitor { pointcut saveLocale(): execution(* MyApplication.onCreate()); pointcut checkLocale(AppCompatActivity activity): this(activity) && execution(* AppCompatActivity+.onCreate(..)); after(): saveLocale() { saveCurrentLocale(); } before(AppCompatActivity activity): checkLocale(activity) { if (isLocaleChanged()) { saveCurrentLocale(); restartApplication(activity); } } void saveCurrentLocale() {/* implementation */} void restartApplication(AppCompatActivity context) {/* implementation */} boolean isLocaleChanged() {/* implementation */} }
Добавив этот класс в наше приложение, мы научим его перезапускать само себя при смене языка в системе.
Некоторое время я искал готовые решения и пробовал их на вкус. В итоге написал свой вариант. Как это всё работает, зачем пришлось изобретать велосипед и что из всего этого вышло — под катом!
Следует оговориться, что выше описан лишь общий каркас аспекта, вся механика заставляет дописать несколько лишних строк советам, чтобы брать данные контекста из срезов и точек соединения. Полный код аспекта вы найдёте в демо-приложении, в конце статьи. Пример иллюстрирует компактность и лаконичность инъекций, благодаря которым аспектный класс пронизывает наше приложение.
Философия
Из этой оговорки следует, что аспектный класс представляет собой декоратор над объектным классом. Подходя к задаче реализации некоторой функциональности на аспектах, важно не забывать об объектно-ориентированных механизмах языка и идеологии как таковой. Bad way — когда аспект превращается в нечитабельный и напичканный служебными инструментами класс. Best way — описать всю логику отдельным объектным модулем, максимально изолированным от внешнего мира, который подключается к приложению через аспектный декоратор.
Говоря об аспектном подходе к разработке как о некоем декорирующем механизме, следует отметить его основные возможности, которые были частично продемонстрированы в примере. Понятие процедур, функций и методов заменяется термином «совет» (advice). Совет может быть применён до (before), после (after) или вместо (around) того участка кода, куда мы встроились через срез. В свою очередь, понятие среза (pointcut) скрывает под собой некоторое описание точки(ек) в нашей программе, куда аспектный класс подключается. Это описание представляет собой целый набор параметров — имя класса и/или сигнатура метода, место его вызова или исполнения, и т.п. Один срез может описывать минимум одну точку соединения (joinPoint), но максимум не ограничивается и может пронизывать всё приложение.
Тестирование
Еще одна область, где аспекты могут себя проявить во всей красе — тестирование и отладка. Написать универсальный профайлер методов и классов легче, чем может показаться.
package com.archinamon.example.xpoint; aspect MyProfilerImpl extends Profiler { private pointcut strict(): within(com.archinamon.example.*) && !within(*.xpoint.*); pointcut innerExecution(): strict() && execution(!public !static * *(..)); pointcut constructorCall(): strict() && call(*.new(..)); pointcut publicExecution(): strict() && execution(public !static * *(..)); pointcut staticsOnly(): strict() && execution(static * *(..)); private pointcut catchAny(): innerExecution() || constructorCall() || publicExecution() || staticsOnly(); before(): catchAny() { writeEnterTime(thisJoinPointStaticPart); } after(): catchAny() { writeExitTime(thisJoinPointStaticPart); } } abstract aspect Profiler issingleton() { abstract pointcut innerExecution(); abstract pointcut constructorCall(); abstract pointcut publicExecution(); abstract pointcut staticsOnly(); protected static final Map<String, Long> sTimeData = new ConcurrentHashMap<>(); protected void writeEnterTime(JoinPointю.StaticPart jp) {/* implementation */} protected void writeExitTime(JoinPoint.StaticPart jp) {/* implementation */} }
Срез strict() урезает обход точек соединения, чтобы исключить обход самих аспектных классов. В остальном описанная структура предельна проста и интуитивно понятна. Я намеренно разделяю выборку методов, конструкторов и статических методов в разные срезы — это позволит гибко настраивать профайлер под конкретное приложение и конкретную задачу. Маркер issingleton() в описании абстрактного аспектного класса явно декларирует, что каждый наследник будет являться синглтоном. По сути, запись ненужная, т.к. все аспектные классы являются синглтонами по умолчанию. В нашем же случае этот маркер здесь нужен, чтобы проинформировать об этом свойстве стороннего разработчика. В своей практике я предпочитаю маркировать неявный функционал — таким образом повышая понимание и читаемость модуля для других.
Перейдём непосредственно к тестированию. Чем аспекты эффективны?
- В первую очередь своей внутренней механикой. Мы тестируем секцию кода в её привычной среде обитания, а не пытаемся эмулировать контекст применения конкретного функционала (функции, процедуры, самого объекта и т.п.).
- Второй важный плюс — богатство отладочной информации, имеющейся в точке соединения благодаря инъекциям на этапе компиляции.
- Третий и очень важный плюс кроется в силе так называемого NamePattern, которым описываются все параметры среза. Нейм-паттерном можно покрыть огромное количество однотипных и похожих участков (например, захватить все геттеры-сеттеры срезом в одну строчку).
Всё это даёт большой профит при написании юнит- и функциональных тестов. Но всегда есть важное «но». Тестирование на аспектах скорее является анализом в режиме реального времени. Для реализации классического цикла тестирования всё равно необходимо описать некоторое окружение или контекст, в котором будут исполняться тестировочные декораторы. А значит, в качестве замены привычным фреймворкам АОП не подойдёт.
Резюмирую всё вышесказанное. Аспектный подход хорошо подойдёт в качестве дополнения к классическому тестированию для покрытия анализаторами и мониторами рабочего продукта с целью выявления ошибок в уже работающем приложении. Например, при ручном тестировании или в виде бета-версии приложения.
abstract aspect NetworkProtector { abstract pointcut myClass(); Response around(): myClass() && execution(* executeRequest(..)) { try { return proceed(); } catch (NetworkException ex) { Response response = new Response(); response.addError(new Error(ex)); return response; } } }
Это самый простой вариант, можно и сложнее, описывая детально и по шагам все этапы.
Велосипед v3.14: зачем и как?
Освоить АОП можно и без Андроида. Чтобы использовать технологию в своём проекте, я занялся написанием собственного плагина к билд-системе Gradle. Но ведь уже есть готовые решения, скажет осведомлённый читатель! И будет прав. И будет не полностью прав, т.к. все имеющиеся подходили лишь под какой-то узкий спектр условий, но не покрывали все необходимые и возможные комбинации. Например, ни один плагин не умел корректно работать с флаворами или создавать свой source-set для декомпозиции исходного кода. А некоторые позволяли писать аспекты только в стиле java-аннотаций. Вот я и собрал все грабли на пути к внедрению собственного плагина. Который, в результате, покрыл все узкие места.
И даже обзавёлся хакнутым семантическим плагином (привет Spring@AOP’у из Ultimate версии!) для личного удобства.
Labels: Type-Enhancement
Subcomponent-Tools-Studio Subcomponent-Tools-gradle-ide
Priority-Small Target-1.6
Отмечу несколько очевидных и не очень вещей, с которыми мне пришлось столкнуться при разработке.
-
Организация стэка задач компилятораЗдесь и поддержка инструментов препроцессинга, и Retrolambda доставила головной боли. Это была первая и, субъективно, самая сложная грабля. Я впервые взялся за написание расширения для Gradle, и все подводные камни с управлением стэком задач собрал на первом же этапе разработки. По итогу плагин явным образом проверяет проект на подключение Retrolambda к нему и, если результат положительный, — встраивается в очередь перед его таском. В противном случае — становится в очередь сразу за java-компилятором.
-
Перфоманс и incremental buildПравильно организовать стэк задач — половина дела. Оптимизировать его и дать фору перфомансу — задача другого уровня. AspectJ подключается вместе с собственным компилятором — ajc. И поэтому всё обрабатывается руками. Скажу честно, здесь еще есть куда развиваться плагину. Задачи кодогенерации препроцессором, сборки java-исходников, сборка dex-файлов (исполняемые файлы Android среды) работают в привычно оптимизированных условиях. При этом ajc всё еще работает не в инкрементальном режиме.
-
Адаптация воркспейса и Gradle-инструментовСначала я реализовал базовые фичи и поддержку популярных плагинов. Исходный код компилируется, аспекты встраиваются, приложение билдится. В проекте, который стал тестовым полигоном, подступала страшная дата внедрения флаворов. Понял, что весь плагин станет неактуальным, если не сумеет подружиться с этими инструментами. А параллельно хотелось удобно и лаконично обустроить рабочую область с исходниками. Вскоре плагин научился работать с билд-вариантами, правильно встраиваться в их задачи. А в конце обзавёлся собственной папкой в ресурсах — aspectj, наравне с java, groovy, aidl и другими.
Что взять с собой?
До кучи к аспектам берём еще синтаксис Java 8 и StreamAPI из той же восьмёрки (это нужно для функциональной работы с массивами, коллекциями и списками) — ведь в Android API уже включён Java API, и новшествами из восьмёрки он, увы, не хвастается.
buildscript { repositories { mavenCentral() maven { url 'https://raw.github.com/Archinamon/GradleAspectJ-Android/master/' } maven { url 'https://raw.github.com/Archinamon/RetroStream/master/' } } dependencies { //retrolambda classpath 'me.tatarka:gradle-retrolambda:3.2.3' //aspectj classpath 'com.archinamon:AspectJ-gradle:1.0.16' } } // Required because retrolambda is on maven central repositories { mavenCentral() } apply plugin: 'com.android.application' //or apply plugin: 'java' or even apply plugin: 'com.android.library' apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.archinamon.aspectj' dependencies { compile 'com.archinamon:RetroStream:1.0.4' }
На этом — всё! Подробную настройку я оставлю за кулисами, но все детали вы сможете найти в исходниках на гитхабе.
Ссылки
Демонстрационный проект.
Gradle-плагин для Android Studio.
ссылка на оригинал статьи http://habrahabr.ru/post/269371/
Добавить комментарий