In Tokopedia, we design many systems that need to be changed on a big scale. This could be seen in how Tokopedia’s android app communicates with server instances using json and several other formats such as protocol buffer. Particularly in json, we could add several values such as string, integer, an array of objects, etc. Some server instances such as Google Tag Manager require our developer to pass in several values, as depicted in figure 1.
As you can see in figure 1, the object of mFirebaseAnalytics has logEvent with a bundle parameter. Bundle is equivalent to Map, but it exists in the android ecosystem.
But there are some challenges for us to use mFirebaseAnalytics, one of them is some misformat Bundle that the android app sent being discarded by the libraries and therefore not getting populated by the dashboard. Also, The misformat might only be found after several releases, as value don’t get shown at the dashboard.. So from that problem, we start to handle the problem properly.
Options that we have
The problem above, we could solve it using 3 options below:
- If-else code style
- Manually created Design Pattern
- Automatically Design Pattern
As you can see at figure 2, we can reformat the bundle or map using standard static functions. But complexity and maintainability cost would come high as we need to know which part should be changed when adding new data format.
Design Pattern which manually created
In figure 3, we can also use factory pattern after the FormatterBundle object is created, we will get the map or bundle. But here we can see the abstraction can separate and decrease the complexity of codes.
But here also we face a new challenge, as our codes grows then we need to manually add the code into it as it will become repetitive.
Design pattern which automatically generated
In previous attempts, we could avoid the repetitive task of adding manually the code by using an annotation processor. This will benefit us as developer especially misformat data got the error at compile time.
Before we begin producing the annotation processor code, we will introduce some terms first:
- Annotated class: a class that has been annotated as shown at figure 4.
2. Annotation class: class that would be used in annotation, from figure 4, we could see Formatter annotated with NotThreadSafe annotation.
3. Annotated field: fields of class that belongs to a class that been annotated
After we understand the basic term, then we must determine what we want to achieve:
- How keys that must be sent to the server instance get checked, for example in figure 3 and figure 2, we understand ITEM_LIST must be sent?
- How do we check the values supplied, for example in figure 3 and figure 2, we understand that ITEM_LIST should contain either `transaction` or `search result`?
- How do we override the value for null values?
How keys that must be sent to the server instance get checked?
First, We can declare annotation classes which are depicted in figure 5.
In figure 5, we can see that annotation class AnalyticEvent can be used to annotate the class and it accepts 3 parameters. nameAsKey means the generated class could use the field name as key. rulesClass is a place which developer place the rules. In order to check whether annotated class have some required values such as ITEM_LIST, then we required the developer to pass the rules in the 3rd parameter.
In figure 6, we see ProductListClickRules that will check 3 values in root scope eventAction, event and item_list. If the annotated class that uses these rules doesn’t define the key in annotated class such as eventAction, then the generated class wouldn’t compile errors.
As in figure 7, we pieced them together in an annotated class named ProductListClick. In ProductListClick, we enable nameAsKey so that the variable name would be key. Thus rules will infer the variable then check against the rules. In this context, because item_list is a required key then it will check against other fields.
Also in figure 8.a, it showed an error message for AddToCart class, which triggers KaptException.
How do we check if the values for certain keys are valid at runtime?
As I’ve mentioned it before, we need to create the annotation for the field. In figure 10, we also need to declare CustomChecker that accepts 3 parameters. The annotated class depicted in figure 11 uses CustomChecker shown in figure 10.
The entire process also shown at figure 12.
How do we override the value for null values?
Like before, we need to create the annotation for the field. Here, we declare DefaultValueSting that would temper the value if value supplied is null.
The usage of DefaultValueString in the annotated class is shown in figure 14.
Last but not least
After solving 3 challenges, we could see that in the future we could simplify or add features without having to rely on certain functions or classes that were manually created. This DRY (Don’t Repeat Yourself) Principle helps us to do more things besides this as the library serves as a layer for that. Also, our code is becoming more composable based on annotations that is used in the annotated classes..