Теорія і практика AOP. Як ми це робимо в Яндексі

Одна з ключових особливостей роботи в Яндексі — це свобода вибору технологій. В Авто.ру, де я працюю, нам доводиться підтримувати великий пласт історичних рішень, тому будь-яка нова технологія або бібліотека зустрічається двома питаннями колег:

— Наскільки це збільшить дистрибутив?
— Як це допоможе нам писати менше і ефективніше?



Зараз ми використовуємо 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. Чому? Очевидність і лаконічність, Карл. Очевидність і лаконічність.

and oop aop

Інструментарій
Поговоримо окремо і коротко про наш разработческий арсенал. В середовищі 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 означає доступ до поля на запис, а в кінці — патерн методу-сетера
}



У більш строгих ситуаціях можна взагалі відмовитися збирати проект, якщо розробник явно «халтурить» або намагається змінити поведінку там, де цього робити явно не слід.

Перевірка на вилов NPE і виклик конструктора за межами білд-методу
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, *;
// не обов'язково писати назви цілком, кругом патерни!
}

Просто про це часто запитують :) Так, можна ручками налаштувати «важливість» і черговість кожного окремого аспекту.
Але не варто пхати це в кожен клас, інакше порядок вийде непередбачуваний (ваш кеп!).


Висновки
Будь-яка задача вирішується найбільш зручними інструментами. Я виділив декілька простих повсякденних задач, які можуть бути легко вирішені з допомогою аспектно-орієнтованого підходу до розробки. Це не заклик відмовитися від ООП і освоювати щось інше, скоріше навпаки! В умілих руках АОП гармонійно розширює об'єктну структуру, вдало вирішуючи завдання ізоляції, дедуплікаціі коду, легко справляючись з копипастой, сміттям, неуважністю при використанні перевірених рішень.

Ми пишемо один коротенький клас у десяток рядків і впроваджуємо його в два десятки інших класів через прозорі і прості «якоря» або умови. При цьому витрати на опис стійких аспектних класів тим нижче, чим швидше ми самі адаптуємося до пошуку і застосування однорідностей в своєму коді.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.