/ EXERCISE, PROGRAMMING, STYLE, AOP

Exercises in Aspect-Oriented Programming Style

This week, we will focus on Aspect-Oriented Programming, a powerful programming technique:

In computing, Aspect-Oriented Programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification, such as "log all function calls when the function’s name begins with 'set'". This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. AOP forms a basis for aspect-oriented software development.

This is the 12th post in the Exercises in Programming Style focus series. Other posts include:

  1. Introducing Exercises in Programming Style
  2. Exercises in Programming Style, stacking things up
  3. Exercises in Programming Style, Kwisatz Haderach-style
  4. Exercises in Programming Style, recursion
  5. Exercises in Programming Style with higher-order functions
  6. Composing Exercises in Programming Style
  7. Exercises in Programming Style, back to Object-Oriented Programming
  8. Exercises in Programming Style: maps are objects too
  9. Exercises in Programming Style: Event-Driven Programming
  10. Exercises in Programming Style and the Event Bus
  11. Reflecting over Exercises in Programming Style
  12. Exercises in Aspect-Oriented Programming Style (this post)
  13. Exercises in Programming Style: FP & I/O
  14. Exercises in Relational Database Style
  15. Exercises in Programming Style: spreadsheets

AOP in Python

To apply AOP in Python is just a matter of:

  1. referencing a function
  2. wrapping it into another function - the advice
  3. and overriding the reference to the original function with this new one

This translates into the following in the original Python code:

def sort(word_freq):                                             (1)
    return sorted(word_freq.items(), key=operator.itemgetter(1), reverse=True)

def profile(f):                                                  (2)
    def profilewrapper(*arg, **kw):                              (3)
        start_time = time.time()
        ret_value = f(*arg, **kw)                                (4)
        elapsed = time.time() - start_time
        print("%s(...) took %s secs" % (f.__name__, elapsed))
        return ret_value
    return profilewrapper                                        (5)

globals()[sort.__name__]=profile(sort)                           (6)
1Define any "standard" function
2Define the advice as a function
3The code itself is a function, because it will replace the original function
4Call the original function with the same arguments passed to this one
5Return the profilewrapper function as the value, which makes it a higher-order function
6Replace the reference to the original sort() function with a function wrapping it

Because Python is an interpreted language, the process is pretty straightforward.

Porting the Python code to Kotlin

Porting the original code to Kotlin yields the following:

var sort = fun(frequencies: Map<String, Int>) = frequencies      (1) (2)
  .map { it.key to it.value }
  .sortedByDescending { it.second }

fun <T, R> profile(f: (T) -> R): (T) -> R {
  return fun(arg: T): R {                                        (2)
    val start = System.currentTimeMillis()
    val result = f.invoke(arg)
    val elapsed = start - System.currentTimeMillis()
    print("$f took $elapsed ms")
    return result
  }
}

sort = profile(sort)                                             (3)
1Unlike in Python, functions cannot be reassigned in Kotlin. So, we need to create an anonymous function, and assign it to a var that can be reassigned
2As in JavaScript, Kotlin allows to create anonymous functions
3Reassign the var

This straightforward porting works but obviously has some downsides:

Calling the sort function

Because it’s a variable of type KFunction1 - and not a function anymore - the following code becomes the standard way to call it:

sort.invoke(mapOf<String, Int>())

Fortunately, it’s possible to use the shorthand form of operators, which looks a lot like standard function invocation (but is not):

sort(mapOf<String, Int>())
Multiple wrapper functions

The signature of the profiling function is profile(f: (T) → R): (T) → R. The parameter and the return value are a function that accepts a single parameter of type T. In real codebases, there will be functions that need profiling and take a different number of parameters _e.g. none, or more than one. Hence, we need to create a dedicated wrapping function for each arity:

  1. profile0(f: () → R): () → R
  2. profile1(f: (T) → R): (T) → R
  3. profile2(f: (T, U) → R): (T, U) → R
  4. profile3(f: (T, U, V) → R): (T, U, V) → R
  5. etc.

The benefits of this approach is that everything is explicit. One just need to read the source code to notice the profile() function to understand it wraps the original function to execute additional code. In Functional Programming, this is known as Function Composition. More functions can be composed, to add even more additional features.

However, reassigning the sort variable is contrary to Functional Programming’s principles. The above code should be reworked a bit to get more "Kotlinesque" code:

profile1<Map<String, Int>, List<Pair<String, Int>>>{ sort(mapOf()) }

Even then, there’s a con to the pro of explicitness: because one needs to explicitly call the composing profile1() function, it’s easy to forget about it. If the function is not called, no profiling will take place, much to the chagrin of the Ops team.

"True" AOP

Before Functional Programming became widespread in the industry, AOP was already available on the JVM through bytecode manipulation. There are two different flavors:

  1. Build-time weaving: the bytecode that is generated from the source code is changed according to an "outside source".
  2. Load-time weaving (LTW): the bytecode that is loaded by the JVM is changed according to an "outside source"

AspectJ is an AOP framework on the JVM able to do both. The outside source described above is structured along the following lines:

  1. Advice: a piece of code that will be applied e.g. the profiling code above
  2. Pointcut: the specification of where to apply the advice. Pointcuts come in different flavors. For example, there’s a before pointcut that run the advice’s code before the actual code - and its mirror, the after pointcut. Another pointcut is applied around a function call. This is exactly what we need to do for profiling. Without further ado, here’s the source code for that:
@Aspect
class AspectUtils {

  @Around("execution (* ch.frankel.blog.eps..*(..))")                    (1)
  fun profile(joinPoint: ProceedingJoinPoint): Any? {                    (2)
    val start = System.currentTimeMillis()                               (3)
    val result = joinPoint.proceed()                                     (4)
    println("$joinPoint took ${System.currentTimeMillis() - start} ms")  (3)
    return result
  }
}
1Advice should be applied around the execution of every function in the ch.frankel.blog.eps package
2The ProceedingJoinPoint parameter will be passed by AspectJ
3Profiling code just as above
4Execute the original wrapped function. Notice there’s no need to pass the actual parameters, it’s all taken care of by the framework

To make this work, one need the actual AspectJ runtime’s library as a dependency, as well as the JCabi Maven plugin to achieve build-time weaving.

The benefit of this approach is that developers don’t need to concern themselves with the profiling code: a team member can write the AOP code, and configure it. It takes one aspect at the end or in parallel of the development process, and all necessary functions are now automatically profiled! Even better, with LTW, one can generate a standard JAR, and the aspect(s) can be applied through a Java agent at runtime (this is not the way chosen here). In that regard, AOP is a great and proven way to address cross-cutting concerns e.g. profiling, logging, etc. to a codebase, and to cut through all the repetitive task of actually calling the required code.

Just be aware that despite this obvious pro, some people are actively advocating against AOP because of the "black magic" involved.

Conclusion

Because Python is an interpreted language, AOP is just a matter of replacing the original function reference with a new one. In Kotlin - and in most JVM languages, there are three options for AOP:

  1. Function Composition, where developpers need to explicitly wrap the function
  2. Build-Time Weaving via AspectJ that applies the advices at build-time, allowing decoupling between the "business code" and a cross-cutting concern
  3. LTW also via AspectJ that applies the advices at runtime, further reducing the coupling and allowing the JAR to be the exact product of the source code
The complete source code for this post can be found on Github.
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Exoscale. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More