Aspect-Oriented Programming: is it useful?
What is Aspect-Oriented Programming (AOP)? #
The list of programming paradigms seems endless, so why not one more?
Motivating AOP #
Software modules have some core purpose, but also have other “aspects of concern” that aren’t directly related to that purpose, but are still necessary (like logging).
The primary motivation for AOP is that when these aspects are cross-cutting, they aren’t easy to organize using typical software design (such as using modules).
Common aspects include:
- Logging
- Caching
- APM / performance monitoring
Origins #
Back in 1996 at Xerox PARC, Kiczales et al.1 published the initial proposal for Aspect-Oriented Programming (AOP). In their words:
AOP works by allowing programmers first to express each of a system’s aspects of concern in a separate and natural form, and then automatically combine those separate descriptions into a final executable form […]
For context, the leading implementation of AOP is as a Java extension called AspectJ. Most of the AOP terminology started there. Here are some important terms (borrowed from here):
- Aspect: an encapsulation of a cross-cutting concern
- Join point: a point in program execution where a cross-cutting concern can happen
- Advice: the actual code action or effect executed at a join point
- Pointcut: an expression to match a join point
Example #
Here’s a simple program to motivate where AOP can be useful.
import logging
logger = logging.getLogger(__name__)
def feed_the_cat():
# Lock so that only one person is feeding the cat.
acquire_food_access_lock()
current_time = get_current_time()
if current_time.hour >= 6 and current_time.hour < 9:
logger.info(f"Breakfast time for cat!")
elif current_time.hour >= 12 and current_time.hour < 15:
logger.info(f"Lunch time for cat!")
else:
logger.info(f"Dinner time for cat!")
food = get_food(db_session)
if food.quantity == 0:
send_alert("No more food!")
else:
food.dispense()
ring_bell()
release_food_access_lock()
def feed_the_dog():
# Lock so that only one person is feeding the dog.
acquire_food_access_lock()
current_time = get_current_time()
if current_time.hour >= 6 and current_time.hour < 9:
logger.info(f"Breakfast time for dog!")
elif current_time.hour >= 12 and current_time.hour < 15:
logger.info(f"Lunch time for dog!")
else:
logger.info(f"Dinner time for dog!")
food = get_food(db_session)
if food.quantity == 0:
send_alert("No more food!")
else:
food.dispense()
ring_bell()
release_food_access_lock()
The core functionality is around feeding a cat or dog, but the implementation also has logging, a resource lock, and some side effects like sending an alert or ringing a bell. These aren’t quite organizable into a module, because they’re cross-cutting.
Applying AOP-style logic #
Ok, so let’s try to use AOP to improve our example. Python doesn’t quite have an AspectJ equivalent, but we can try an intriguing project called Blinker. Blinker is a library for dispatching in-process signals in a Python application.
We’ll make use of signals to handle these cross-cutting concerns. An approximate mapping of AOP concepts to Blinker signals:
- The
signal
connection is the pointcut - The advice is the signal receiver/handler function
- Within the main code, the join point is the
send
call
import logging
from blinker import signal
logger = logging.getLogger(__name__)
preparing_for_feeding = signal('preparing_for_feeding')
dispensing_food = signal('dispensing_food')
feeding_concluded = signal('feeding_concluded')
@preparing_for_feeding.connect
def on_preparing_for_feeding(sender, animal):
# Lock so that only one person is feeding the animal.
acquire_food_access_lock()
current_time = get_current_time()
if current_time.hour >= 6 and current_time.hour < 9:
logger.info(f"Breakfast time for {animal}!")
elif current_time.hour >= 12 and current_time.hour < 15:
logger.info(f"Lunch time for {animal}!")
else:
logger.info(f"Dinner time for {animal}!")
@dispensing_food.connect
def on_dispensing_food(sender, animal, food):
logger.info(f"Attempting to dispense food for {animal}...")
if food.quantity == 0:
send_alert(f"No more food for {animal}!")
else:
ring_bell()
@feeding_concluded.connect
def on_feeding_concluded(sender, animal):
logger.info(f"Feeding time for {animal} ended.")
release_food_access_lock()
@out_of_food.connect
def on_out_of_food(sender, animal):
send_alert(f"No more food for {animal}!")
def feed_the_cat():
preparing_for_feeding.send("cat")
food = get_food()
dispensing_food.send("cat", food)
food.dispense()
feeding_concluded.send("cat")
def feed_the_dog():
preparing_for_feeding.send("dog")
food = get_food()
dispensing_food.send("dog", food)
food.dispense()
feeding_concluded.send("dog")
That reduces code duplication a bit, and it’s clearer where the cross-cutting concerns are and where the core logic is.
Caveats #
AOP isn’t a magical solution – there are several practical concerns.
First, there really isn’t language support for AOP, and languages might not even have third party libraries for it.
Second, one could argue AOP actually worsens the complexity of the code. This is because now the cross-cutting logic is scattered and indirected, making the code harder to reason about.
Also, the infrequency of AOP in practice means there aren’t many conventions, let alone examples. When dealing with software design, consistency and maintainability are important. With AOP, there aren’t many references to follow to grow the codebase in a sensible way.
Like a lot of less common paradigms, AOP is a unique way to examine software structure and a good thinking exercise. That said, it’s likely to remain a nice thought instead of a practical path forward.
-
Lopes, Cristina & Kiczales, Gregor & Lamping, John & Mendhekar, Anurag & Maeda, Chris & Loingtier, Jean-marc & Irwin, John. (1999). Aspect-Oriented Programming. ACM Computing Surveys. 28. 10.1145/242224.242420. ↩︎