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}
    

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

    Если кто-то вдруг надумает взяться и довести до ума эту штуку то милости прошу сюда.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 3
    • +2
      А почему overhead этого решения не описали? )
      • +1
        Картинка к статье намекает, что никаких бенчмарков к решению не прилагается:)
        • +1
          Идея интересная, возможно ее стоит развить. Ускорить можно кодогенерацией.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.