Одна из ключевых особенностей работы в Яндексе — это свобода выбора технологий. В Авто.ру, где я работаю, нам приходится поддерживать большой пласт исторических решений, поэтому любая новая технология или библиотека встречается двумя вопросами коллег:
— Насколько это увеличит дистрибутив?
— Как это поможет нам писать меньше и эффективнее?
Сейчас мы используем RxJava, Dagger 2, Retrolambda и AspectJ. И если о первых трёх технологиях слышал каждый разработчик, а многие даже применяют их у себя, то о четвёртой знают только хардкорные джависты, пишущие большие серверные проекты и разного рода энтерпрайзы.
Передо мной стояла цель ответить на эти два вопроса и обосновать использование AOP-методологии в Android-проекте. А это значит — написать код и показать наглядно, как аспектно-ориентированное программирование поможет нам ускорить и облегчить работу разработчиков. Но обо всём по порядку.
Начнём с азов
Хотим обернуть все запросы к API в трай-кетч, и чтоб никогда не падало! А ещё логи! А ещё...
Пфф… Пишем семь строчек кода и вуаля.
abstract aspect NetworkProtector { // аспектный класс, по умолчанию синглтон
abstract pointcut myClass(); // срез, он же — поиск мест внедрения нижележащих инструкций
Response around(): myClass() && execution(* executeRequest(..)) { // встраиваемся «вместо» методов executeRequest
try {
return proceed(); // выполняем само тело метода, перехваченного around'ом
} catch (NetworkException ex) {
Response response = new Response(); // если сервер не умеет в обработку ошибок...
response.addError(new Error(ex)); // …ну или сетевой слой написан, мягко говоря, не очень
return response;
}
}
}
Легко, правда? А теперь немного терминологии, без неё дальше никак.
Аспектное программирование — это изоляция кода в отдельные и независимые модули. В обычном объектном подходе этот код пронизывал бы всё приложение (или его значительную часть), встречаясь на каждом шагу, как примеси в чистой логике компонентов. Такими примесями могут быть перзистенция, контроль прав доступа, логирование и профилирование, маркетинговая и девелоперская аналитика.
Первое, с чего разработчик начинает постигать дзен, — поиск однородностей. Если два класса делают сколько-нибудь похожую работу, например оперируют одним и тем же объектом, — они однородны. Когда n сущностей абсолютно одинаково взаимодействуют с внешним миром — они однородны. Всё это можно описать срезами (pointcut) и начать увлекательный путь к просвещению.
Второе, без чего не обходится ни один просветлённый инженер. Приходится в уме предусматривать все возможные комбинации случайных однородностей. Под ваши условия могут подпасть объекты, вовсе не относящиеся к теме и ситуации. Просто вы не учли, что под каким-то хитрым углом они выглядят похоже. Это научит вас писать устойчивые паттерны.
Лучше всего начать описание срезов с аннотаций. И, честно говоря, лучше ими же закончить. Это прекрасный и очевидный подход, пришедший из пятой джавы. Именно аннотации скажут непросвещённому инженеру, что в этом классе творится какая-то запредельная магия. Именно аннотации являются вторым сердцем Spring-фреймворка, которые разруливает AspectJ под капотом. Этим же путём идут все современные большие проекты — AndroidAnnotations, Dagger, ButterKnife. Почему? Очевидность и лаконичность, Карл. Очевидность и лаконичность.
Инструментарий
Поговорим отдельно и коротко про наш разработческий арсенал. В среде Android великое множество инструментов и методологий, архитектурных подходов и различных компонентов. Здесь и миниатюрные библиотеки-хелперы, и монструозные комбайны типа Realm. И относительно небольшие, но серьёзные Retrofit, Picasso.
Применяя в своих проектах всё это многообразие, мы адаптируем не только свой код под новые архитектурные аспекты и библиотеки. Мы апгрейдим и свой собственный скилл, разбираясь и осваивая новый инструмент. И чем этот инструмент больше, тем серьёзнее приходится переучиваться.
Наглядно эту адаптацию демонстрирует набирающий популярность Kotlin, который требует не столько освоения себя как инструмента, сколько изменения подхода к архитектуре и структуре проекта в целом. Сахарные примеси аспектного подхода в этом языке (я сейчас намекаю на экстеншен методов и полей) добавляют нам гибкости в построении бизнес-логики и перзистенции, но притупляют понимание процессов. Чтобы «видеть», как будет работать код на устройстве, в голове приходится интерпретировать не только видимый сейчас код, но и подмешивать в него инструкции и декораторы извне.
Та же ситуация, когда речь заходит об АОП.
Выбор проблем и решений
Конкретная ситуация диктует нам набор подходящих и возможных (или не очень) решений. Мы можем искать решение у себя в голове, опираясь на собственный опыт и знания. Или же обратиться за помощью, если знаний недостаточно для решения какой-то конкретной задачи.
Пример вполне очевидной и простой «задачи» — сетевой слой. Нам понадобится:
- Изолировать сетевой слой. (Retrofit)
- Обеспечить прозрачное общение с UI-слоем. (Robospice, RxJava)
- Предоставить полиморфный доступ. (EventBus)
И если раньше вы не работали с RxJava или EventBus, решение этой задачи обернётся массой подводных граблей. Начиная от синхронизации и заканчивая lifecycle.
Пару лет назад мало кто из Android-девелоперов знал про Rx, а сейчас он набирает такую популярность, что скоро может стать обязательным пунктом в описании вакансий. Так или иначе, мы всегда развиваем себя и адаптируемся к новым технологиям, удобным практикам, модным веяниям. Как говорится, мастерство приходит с опытом. Даже если на первый взгляд они не особо и нужны были :)
Новые горизонты, или зачем нужен АОП?
В аспектной среде мы видим кардинально новое понятие — однородность. Сразу в примерах и без лишних слов. Но не будем далеко отходить от Android'a.
public class MyActivityImpl extends Activity {
protected void onCreate(Bundle savedInstanceState) {
TransitionProvider.overrideWindowTransitionsFor(this);
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
Toolbar toolbar = ToolbarProvider.setupToolbar(this);
this.setActionBar(toolbar);
AnalyticsManager.register(this);
}
}
Подобный бойлерплейт мы пишем чуть ли не в каждом экране и фрагменте. Отдельные процедуры могут быть определены в провайдерах, презентарах или интеракторах. А могут «толпиться» прямо в системных коллбэках.
Чтобы всё это приобрело красивый и системный (от слова «систематизировать») вид, сперва хорошенько подумаем вот над чем: как нам изолировать такую логику? Хорошим решением здесь будет написать несколько отдельных классов, каждый из которых станет отвечать за свой маленький кусочек.
public aspect ToolbarDecorator {
pointcut init(): execution(* Activity+.onCreate(..)) && // тело метода в любом наследнике Activity
@annotation(StyledToolbarAnnotation); // только с аннотацией над классом или методом
after() returning: init() { // не будем стайлить тулбар, если onCreate крашнулся
Activity act = thisJoinPoint.getThis();
Toolbar toolbar = setupToolbar(act);
act.setActionBar(toolbar);
}
}
public aspect TransitionDecorator {
pointcut init(TransitionAnnotation t): @within(t) && // аннотация мастхэв
execution(* Activity+.onCreate(..)); // уже видели
before(TransitionAnnotation transition): init(transition) {
Activity act = thisJoinPoint.getThis();
registerState(transition);
overrideWindowTransitionsFor(act);
}
}
public aspect AnalyticsInjector {
private static final String API_KEY = “…”;
pointcut trackStart(): execution(* Activity+.onCreate(..)) &&
@annotation(WithAnalyticsInit);
after(): returning: trackStart() {
Context context = thisJoinPoint.getThis();
YandexMetrica.activate(context, API_KEY);
Adjust.onCreate(new AdjustConfig(context, “…”, PROD));
}
}
Ну вот и всё. Мы получили чистый и компактный код, где каждая порция однородной функциональности красиво изолирована и пристёгивается только туда, где она явно нужна, а не в каждый класс, посмевший отнаследоваться от Activity.
Финальный вид:
@StyledToolbarAnnotation
@TransitionAnnotation(TransitionType.MODAL)
@WithContentViewLayout(R.layout.activity_main) // ну прямо как AndroidAnnotations! m/
public class MyActivityImpl extends Activity {
@WithAnalyticsInit
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* ... */
}
}
В данном примере наши аннотации являются источником однородности и с их помощью мы можем «пристегнуть» дополнительную функциональность буквально в любом нужном месте. Комбинируя аннотации, самодокументируемые названия и смекалку, мы ищем или декларируем однородности вдоль всего объектного кода, как бы аугментируя и декорируя его нужными инструкциями.
Благодаря аннотациям сохраняется понимание процессов, происходящих в коде. Новичок сразу смекнёт, что здесь подкапотная магия. Самодокументируемость может позволить нам легко управлять служебными инструментами — логированием, профилированием. Инструментирование Java-кода можно легко настроить на поиск по вхождениям ключевых слов в именах классов, методов или полей, доступ и использование которых хотим отследить.
О нестандартных аспектах применения аспектов.
Большие команды часто выстраивают строгий flow коммита, по которому код проходит множество этапов. Здесь могут быть тестовые сборки на CI, инспекция кода, обкатка тестами, pull-request. Количество итераций в этом процессе можно сократить без потери качества путём введения статического анализа кода, для которого вовсе не обязательно устанавливать дополнительное ПО, заставлять разработчика изучать lint-репорты или выносить этот кейс на сторону того же svc.
Достаточно описать директивы компилятору, который сумеет сам определить, что именно в нашем коде делается «неправильно» или «потенциально плохо».
public aspect AccessVerifier {
declare warning : fieldSet() && within(ru.yandex.example.*)
: "writing field outside setter" ;
pointcut fieldSet(): set(!public * *) && !withincode(* set*(..));
// set означает доступ к полю на запись, а в конце — паттерн метода-сеттера
}
В более строгих ситуациях можно вообще отказаться собирать проект, если разработчик явно «халтурит» или пытается видоизменить поведение там, где этого делать явно не следует.
public aspect AnalyticsVerifier {
declare error : handler(NullPointerException+) // декларация try-catch блока с обработкой NPE
&& withincode(* AnalyticsManager+.register(..))
: "do not handle NPE in this method";
declare error : call(AnalyticsManager+.new(..))
&& !cflow(static AnalyticsManager.build(..))
: "you should not call constructor outside a AnalyticsManager.build() method";
}
Магическое слово «cflow» — это захват всех вложенных вызовов на любой глубине в пределах выполнения целевого метода. Не слишком очевидная, но очень мощная штука.
public aspect StrictVerifyOrder {
// сначала инжекторы/декораторы, потом проверяем что да как
declare precedence: *Injector, *Decorator, *Verifier, *;
// не обязательно писать названия целиком, кругом паттерны!
}
Просто об этом часто спрашивают :) Да, можно ручками настроить «важность» и очерёдность каждого отдельного аспекта.
Но не стоит пихать это в каждый класс, иначе порядок получится непредсказуемый (ваш кэп!).
Выводы
Любая задача решается наиболее удобными инструментами. Я выделил несколько простых повседневных задач, которые могут быть легко решены с помощью аспектно-ориентированного подхода к разработке. Это не призыв отказаться от ООП и осваивать что-то другое, скорее наоборот! В умелых руках АОП гармонично расширяет объектную структуру, удачно разрешая задачи изоляции, дедупликации кода, легко справляясь с копипастой, мусором, невнимательностью при использовании проверенных решений.
Мы пишем один коротенький класс в десяток строк и внедряем его в два десятка других классов через прозрачные и простые «якоря» или условия. При этом затраты на описание устойчивых аспектных классов тем ниже, чем быстрее мы сами адаптируемся к поиску и применению однородностей в своём коде.
Автор: Яндекс