AOP или как написать свой велосипед для аналитики

от автора

image
В крупных проектах, при реализации логики трекинга событий, часто встают перед проблемой загрязнения кода вызовами методов трекинга, неудобством явного связывания объектов с событиями и поддержкой этих событий при изменении моделей или ui поведения
Из-за вышеописанных причин, мне пришло в голову написать свой решение, которое конечно же, не уйдет дальше моего git репозитория и этой статьи 🙂

Кто не боится рефлексии и медленного кода прошу под кат

Может не нужно?

В продуктовых приложениях от того, сколько раз тапнут на эту кнопку или как низко прокрутят список зависит развитие приложения как продукта, его внешний интерфейс и функциональность
Из за этого, код начинают разрезать вызовы методов для трекинга событий, которые разбросаны по всему представлению и еще учитывают состояние определенных объектов
Я хочу сделать менее болезненным добавление новых событий и решить несколько проблем, взятых с воздуха с которыми мне приходилось сталкиваться

Я хочу добиться следующего
1) Минимальное количество кода для нового события
2) Минимально количество кода в представлении
3) Удобная система привязки обьектов к событиям

Решение строится на аннотациях, рефлексии и аспектах.
Для реализации аспектной части нашего приложения я буду использовать AspectJ. Он является аспектно-ориентированным расширением для языка Java. На данный момент это, наверное, самый популярный AOP движок.
Кстати этот движок был разработан теми самыми людьми, которые и предложили парадигму аспектов

Как это работает
Чтобы перехватывать вызов нужных нам методов создаем класс помеченный как @Aspect.
Делаее создаем точку соединения с нашими методами и создаем метод помеченный @Around который будет выполняться на точке соединения. AspectJ функционально богат и поддерживает большое количество вариантов точек срезов и советов, но сейчас не об этом

@Aspect public class ViewEventsInjector {     private static final String POINTCUT_METHOD = "execution(@com.makarov.ui.tracker.library.annotations.ViewEvent * *(..))";      @Pointcut(POINTCUT_METHOD)     public void methodAnnotatedWithViewEvent() {     }      @Around("methodAnnotatedWithViewEvent()")     public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {         MethodSignature ms = (MethodSignature) joinPoint.getSignature();         Method method = ms.getMethod();         Object object = joinPoint.getThis();         Object[] arg = joinPoint.getArgs();           /* зная метод, входные параметры и объект класса чей метод вызывали          мы можем получить всю нужную нам информацию   */          Object result = joinPoint.proceed();         return result;     } } 

Реализация

Аннотация для наблюдаемых view

@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) public @interface LoggerView {     String value(); } 

Параметр аннотации — имя view элемента для более удобного чтения событий/логов

В итоге, после инициализации, у нас есть Map в котором лежат id view элементов, отслеживаемых нами

Перехват событий полностью ложится на плечи аспектов
Мы будем ориентироваться на аннотации, которыми у нас помечаны методы view событий

Логика такая:
1) Перехватываем вызов метода
2) Находим его обработчик, который мы добавили в map с всеми возможными обработчиками методов
3) Находим по параметрам аннотации все обьекты, которые нужно отследить
4) Создаем обьекта Event из наших полученных данных
4) Сохраняем событие

Аннотация для методов, на которые будут повешаны наши события

@Retention(RetentionPolicy.CLASS) @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD }) public @interface ViewEvent {     String[] value();  } 

Чтобы унифицировать модели которые мы хотим привязывать к нашим событиям вводим интерфейс, который должна реализовывать модель

public interface LoggingModel {     Map<String, String> getModelLogState(); } 

Пример реализации интерфейса

public class Artist implements LoggingModel {     private final  String mId;     private final String mName;      public Artist(String id, String name){         mId = id;         mName = name;     }     /*  ...  */     @Override     public Map<String, String> getModelLogState() {         Map<String, String> logMap = new HashMap<>();         logMap.put("artistId", mId);         logMap.put("artistName", mName);         return logMap;     } } 

Собираем все это вместе

Ну и наконец собираем все это и в несколько аннотаций у нас начинают трекаться нужные нам события

public class MainActivity extends AppCompatActivity implements View.OnClickListener, TextWatcher{      public static final String TAG = MainActivity.class.getSimpleName();      @LoggerView("first button")     public Button button;     public Button button2;      @LoggerView("test editText")     public EditText editText;      public Artist artist = new Artist("123", "qwe");     public Track track = new Track("ABS");      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          /*   инициализация view элементов   */         ViewEventsInjector.init();         ViewEventsInjector.inject(this);     }      @Override     @AttachState({"artist","track"})     @ViewEvent(ViewEventsTracker.CLICK)     public void onClick(View v) {         Log.d(TAG, "method onClick - " + v.getId());     }      @Override     public void beforeTextChanged(CharSequence s, int start, int count, int after) {     }      @Override     public void onTextChanged(CharSequence s, int start, int before, int count) {     }      @Override     @AttachState({"artist"})     @ViewEvent(ViewEventsTracker.AFTER_TEXT_CHANGED)     public void afterTextChanged(Editable s) {         Log.d(TAG, "afterTextChanged");     } } 

Запускаем проект, пробуем тапнуть по кнопке, затем ввести что-нибудь в текстовое поле

И видим наши долгожданные логи, без единой строчки логики в представлении

07-13 13:52:16.406   D/SimpleRepository﹕ Event{nameView='fist button', nameEvent='onClick', mModelList=[Artist@52a30ec8, Track@52a31040], methodParameters = null, mDate = Mon Jul 13 13:52:16 EDT 2015} 07-13 13:52:24.254   D/SimpleRepository﹕ Event{nameView='textView', nameEvent='afterTextChanged', mModelList=[Artist@52a30ec8], methodParameters= {text = hello}, mDate=Mon Jul 13 13:52:24 EDT 2015} 

На мой взгляд мы даже этим простым проектиком решили несколько проблем и возможно съэкономили какое то количество времени для рутиных действий
Если потратить еще какое-то количество времени, то можно было неплохо оптимизировать логику аспекта, например немного переделать хранение обьектов, чтобы не получать их каждый раз через рефлексию

Если кто-то вдруг надумает взяться и довести до ума эту штуку то милости прошу сюда

ссылка на оригинал статьи http://habrahabr.ru/post/262381/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *