Pull to refresh

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

Reading time 4 min
Views 7.2K
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}

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

Если кто-то вдруг надумает взяться и довести до ума эту штуку то милости прошу сюда.
Tags:
Hubs:
+7
Comments 3
Comments Comments 3

Articles