AOP or how to write your bike for analytics

    image
    In large projects, when implementing the logic of tracking events, they often face the problem of code pollution by calling tracking methods, the inconvenience of explicitly linking objects to events and supporting these events when changing models or ui behavior.

    Due to the above reasons, it occurred to me to write my own solution, which of course will not go beyond my git repository and this article.

    Who is not afraid of reflection and a slow code - I ask for cat.

    Maybe not necessary?

    In product applications, how many times they tap on this button or how low they scroll the list depends on the development of the application as a product, its external interface and functionality.
    Because of this, the code begins to cut method calls for tracking events that are scattered throughout the view and still take into account the state of certain objects.
    I want to make it less painful to add new events and solve a few problems I’ve encountered from the air .

    I want to achieve the following:
    1) The minimum amount of code for a new event;
    2) The minimum amount of code in the view;
    3) Convenient system of binding objects to events.

    The decision is based on annotations, reflection and aspects.
    To implement the aspect of our application, I will use AspectJ. It is an aspect-oriented extension for the Java language. At the moment, this is probably the most popular AOP engine.
    By the way, this engine was developed by the very people who proposed the paradigm of aspects.

    How it works
    To intercept the call to the methods we need, we create a class marked as @Aspect .
    Next, create a junction with our methods and create a method marked @Around that will be executed on the junction. AspectJ is functionally rich and supports a large number of slice point options and tips, but that's not the point.

    @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;
        }
    }
    


    Implementation

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

    Annotation parameter is the name of the view element for more convenient reading of events / logs.

    As a result, after initialization, we have a Map which contains id view of the elements tracked by us.

    Interception of events falls entirely on the shoulders of the aspects.
    We will focus on the annotations with which we have labeled the event view methods.

    The logic is:
    1) Intercept the method call;
    2) We find its handler, which we added to map with all possible method handlers;
    3) We find all objects that need to be tracked by the annotation parameters;
    4) Create an Event object from our received data;
    4) Save the event.

    Annotation for the methods on which our events will be hung:

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

    To unify the models that we want to bind to our events, we introduce the interface that the model should implement:

    public interface LoggingModel {
        Map getModelLogState();
    }
    

    Interface implementation example:

    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 getModelLogState() {
            Map logMap = new HashMap<>();
            logMap.put("artistId", mId);
            logMap.put("artistName", mName);
            return logMap;
        }
    }
    


    Putting it all together

    Well, finally, putting it all together and in a few annotations we start tracking the events we need.

    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");
        }
    }
    

    We start the project, try to tap on the button, then enter something in the text box.

    And we see our long-awaited logs, without a single line of logic in the view.

    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}
    

    In my opinion, even with this simple project engineer we have solved several problems and perhaps saved some amount of time for routine actions.
    If you spend some more time, then it would be possible to optimize the logic of the aspect well, for example, redo the storage of objects a bit so as not to receive them each time through reflection.

    If someone suddenly decides to take it and bring this thing to mind, then you are welcome here .

    Also popular now: