📜 ⬆️ ⬇️

Theory and Practice of AOP. How we do it in Yandex

One of the key features of work in Yandex is the freedom to choose technologies. In Avto.ru, where I work, we have to maintain a large reservoir of historical solutions, so any new technology or library is encountered by two questions of colleagues:

- How much will it increase the distribution?
- How does this help us write less and more efficiently?


')
Now we use RxJava , Dagger 2 , Retrolambda and AspectJ . And if every developer has heard about the first three technologies, and many even use them in their own home, then only hardcore javista know about the fourth, writing large server projects and various enterprises.

My goal was to answer these two questions and justify the use of AOP-methodology in the Android-project. And that means writing code and demonstrating visually how aspect-oriented programming will help us speed up and facilitate the work of developers. But first things first.



Let's start with the basics


We want to wrap all requests to the API in a tray ketch, and so that it never falls! And also logs! And also ...
Pff ... We write seven lines of code and voila.
abstract aspect NetworkProtector { //  ,    abstract pointcut myClass(); // ,   —      Response around(): myClass() && execution(* executeRequest(..)) { //  «»  executeRequest try { return proceed(); //    ,  around' } catch (NetworkException ex) { Response response = new Response(); //       ... response.addError(new Error(ex)); // …    ,  ,   return response; } } } 


Easy, right? And now a bit of terminology, without it no further.

Aspect programming is the isolation of code into separate and independent modules. In the usual object approach, this code would permeate the entire application (or a significant part of it), meeting at every step, as an impurity in the pure logic of the components. Such impurities can be persistency, access control, logging and profiling, marketing and development analytics.

The first thing a developer begins to comprehend Zen with is the search for homogeneity. If two classes do any similar work, for example, they operate on the same object, they are homogeneous. When n entities interact in exactly the same way with the outside world, they are homogeneous. All this can be described in slices (pointcut) and start a fascinating path to enlightenment.

The second, without which no enlightened engineer can do. It comes to mind to envisage all possible combinations of random homogeneities. Objects that are not related to the subject and situation may fall under your conditions. You just did not take into account that at some tricky angle, they look similar. This will teach you to write sustainable patterns.

It is best to start the description of the slices with annotations. And, frankly, it is better to finish them. This is a wonderful and obvious approach that came from the fifth java. It is the annotations that will tell the unenlightened engineer that some kind of transcendent magic is going on in this class. It is annotations that are the second heart of the Spring framework, which AspectJ resolves under the hood. All modern large projects - AndroidAnnotations, Dagger, ButterKnife, follow the same path. Why? Evidence and conciseness, Karl. Evidence and conciseness.

oop and aop

Tools


Let's talk separately and briefly about our development arsenal. In the Android environment, a great variety of tools and methodologies, architectural approaches and various components. Here and miniature library helper, and monstrous combines like Realm. And relatively small, but serious Retrofit, Picasso.
Applying all this diversity in our projects, we are adapting not only our code to new architectural aspects and libraries. We upgrade and our own skill, understanding and mastering the new tool. And the more this tool, the more serious you have to relearn.

This adaptation is clearly demonstrated by the growing popularity of Kotlin, which requires not so much the development of itself as a tool, but a change in the approach to the architecture and structure of the project as a whole. Sugar impurities of the aspect approach in this language (I am hinting at the extension of methods and fields) add us flexibility in building business logic and persistence, but dull the understanding of the processes. In order to “see” how the code will work on the device, in my head you have to interpret not only the currently visible code, but also mix in instructions and decorators from the outside.

The same situation when it comes to AOP.

Selection of problems and solutions


The specific situation dictates a set of suitable and possible (or not) solutions. We can seek a solution in our head, based on our own experience and knowledge. Or ask for help if knowledge is not enough to solve a particular problem.
An example of an obvious and simple “task” is the network layer. We will need:

And if before you did not work with RxJava or EventBus, the solution to this problem will result in a mass of underwater rake. From synchronization to lifecycle.

A couple of years ago, few of the Android developers knew about Rx, and now it is gaining such popularity that it may soon become a mandatory item in the job description. One way or another, we always develop ourselves and adapt to new technologies, convenient practices, fashion trends. As they say, skill comes with experience. Even if at first glance they were not really needed :)

New horizons, or why you need AOP?


In the aspectual environment, we see a radically new concept - homogeneity. Immediately in the examples and without further ado. But let's not go far from Android'a.

 public class MyActivityImpl extends Activity { protected void onCreate(Bundle savedInstanceState) { TransitionProvider.overrideWindowTransitionsFor(this); super.onCreate(savedInstanceState); this.setContentView(R.layout.activity_main); Toolbar toolbar = ToolbarProvider.setupToolbar(this); this.setActionBar(toolbar); AnalyticsManager.register(this); } } 


We write such a boilerplate almost in every screen and fragment. Separate procedures can be defined in providers, presenters or interaktors. And they can “crowd” right in the system callbacks.
In order for all this to become beautiful and systemic (from the word “systematize”), we first think carefully about what: how do we isolate such logic? A good solution here is to write several separate classes, each of which will be responsible for its own little piece.

We first isolate the behavior of the toolbar.
 public aspect ToolbarDecorator { pointcut init(): execution(* Activity+.onCreate(..)) && //      Activity @annotation(StyledToolbarAnnotation); //        after() returning: init() { //    ,  onCreate  Activity act = thisJoinPoint.getThis(); Toolbar toolbar = setupToolbar(act); act.setActionBar(toolbar); } } 


Now let's get rid of redefining the animations
 public aspect TransitionDecorator { pointcut init(TransitionAnnotation t): @within(t) && //   execution(* Activity+.onCreate(..)); //   before(TransitionAnnotation transition): init(transition) { Activity act = thisJoinPoint.getThis(); registerState(transition); overrideWindowTransitionsFor(act); } } 


And finally - throw analytics into a separate class.
 public aspect AnalyticsInjector { private static final String API_KEY = “…”; pointcut trackStart(): execution(* Activity+.onCreate(..)) && @annotation(WithAnalyticsInit); after(): returning: trackStart() { Context context = thisJoinPoint.getThis(); YandexMetrica.activate(context, API_KEY); Adjust.onCreate(new AdjustConfig(context, “…”, PROD)); } } 



Well that's all. We received a clean and compact code, where each portion of homogeneous functionality is beautifully isolated and is attached only where it is clearly needed, and not in every class that dares to inherit from Activity.
Final View:
 @StyledToolbarAnnotation @TransitionAnnotation(TransitionType.MODAL) @WithContentViewLayout(R.layout.activity_main) //    AndroidAnnotations! \m/ public class MyActivityImpl extends Activity { @WithAnalyticsInit protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* ... */ } } 


In this example, our annotations are a source of homogeneity and, with their help, we can “attach” additional functionality in literally any place. Combining annotations, self-documenting names and ingenuity, we look for or declare homogeneity along the entire object code, as if augmenting and decorating it with necessary instructions.

Thanks to annotations, the understanding of the processes occurring in the code is preserved. The newcomer will immediately realize that there is magic under the hood. Self-documenting can allow us to easily manage service tools - logging, profiling. Java instrumentation can be easily configured to search by keyword occurrences in the names of classes, methods, or fields that we want to track and use.

About non-standard aspects of the application of aspects.

Large teams often build a strict commit commit flow through which the code goes through many stages. There may be test builds on CI, code inspection, test run, pull-request. The number of iterations in this process can be reduced without loss of quality by introducing static code analysis, for which it is not necessary to install additional software, force the developer to study lint reports or take this case to the side of the same svc.

It is enough to describe the directives to the compiler, which will be able to determine for itself what exactly is done in our code “wrong” or “potentially bad”.

A simple check for field recording outside the setter method
 public aspect AccessVerifier { declare warning : fieldSet() && within(ru.yandex.example.*) : "writing field outside setter" ; pointcut fieldSet(): set(!public * *) && !withincode(* set*(..)); // set      ,    —  - } 



In more stringent situations, you can refuse to build a project at all, if the developer is clearly “making a mess” or trying to modify the behavior where it is clearly not necessary to do this.

Checking for NPE trapping and calling a constructor outside the build method
 public aspect AnalyticsVerifier { declare error : handler(NullPointerException+) //  try-catch    NPE && withincode(* AnalyticsManager+.register(..)) : "do not handle NPE in this method"; declare error : call(AnalyticsManager+.new(..)) && !cflow(static AnalyticsManager.build(..)) : "you should not call constructor outside a AnalyticsManager.build() method"; } 

The magic word “cflow” is the capture of all nested calls at any depth within the execution of the target method. Not too obvious, but very powerful.


Order is important to me! What if something works at the wrong time?
 public aspect StrictVerifyOrder { //  /,      declare precedence: *Injector, *Decorator, *Verifier, *; //     ,  ! } 

It's just that people often ask about it :) Yes, you can adjust the “importance” and sequence of each individual aspect with pens.
But do not shove it into each class, otherwise the order will turn out to be unpredictable (your cap!).


findings


Any problem is solved by the most convenient tools. I have identified a few simple everyday tasks that can be easily solved using an aspect-oriented approach to development. This is not a call to abandon the PLO and learn something else, rather the opposite! In the able hands, AOP harmoniously expands the object structure, successfully solving the problems of isolation, deduplication of code, easily coping with copy-paste, garbage, and negligence when using proven solutions.

We write one short class in a dozen lines and implement it in two dozen other classes through transparent and simple “anchors” or conditions. At the same time, the costs of describing stable aspect classes are lower, the faster we ourselves adapt to the search and application of homogeneities in our code.

Source: https://habr.com/ru/post/280117/


All Articles