Aspect-Oriented programming has captivated my imagination for years. C#, however, does not support AOP. Even conceptually realizing benefits of the paradigm proved difficult. Then, I experienced functional composition.
What is Aspect-Orientation
The heart of Aspect-Orientated Programming (AOP) is cross-cutting concerns. The creators saw concerns like logging, authorization, authentication, and transaction management. They asked why these needed to be spread through the program.
- Their implementation is nearly identical everywhere
- They are not intrinsic to the business logic. They distract from the semantic clarity of a function
- They are needed through most components of the system
The idea is to run these cross-cutting concerns as a sort of add-on or interceptor for other logic flows. This allows the cross-cutting concerns to be defined centrally, evolve separately from the core logic, and for the core logic to remain focused.
What is a Decorator
Decorator is an Object-Oriented pattern similar in motivation to AOP. The idea is that you take some core functionality and “decorate” it with additional functionality. The core logic is unchanged and many decorators can be used together to create complex functionality.
A good example (stolen from Steve Tockey) is the Java filestream. At the base is the filestream. Then, you can add a Compressed decorator to get a compressed filestream. Add an encrypted decorator and you get an encrypted compressed filestream. So on and so on.
C# difficulties
AOP requires some mechanism for “weaving” the cross-cutting concerns back into core functionality. C# has no built-in solution for proxying methods or specifying behavior before or after a method call.
That is a bit of a lie. Attributes can be triggered before a function call. They can interrupt execution with an exception, but don’t have access to the function inputs or outputs.
Two options to get around this were
- Post-sharp: runs at build-time by re-writing the compiled IL
- Constructor Injection + Decorators: Injecting abstract types allows me to add decorator functionality in the composition root. Example below.
Post-sharp was expensive, so I looked at constructor injection + decorator. For example
|
|
This works. It works well for relatively isolated scenarios or common shared interfaces (i.e. INotifier). It does not work well for activities that would be shared between many components that don’t share an interface. It would end up requiring decorator implementations for every abstraction in the system.
Some DI frameworks cater to this specific scenario. Autofac and Castle-Windsor allow you to register decorators where the framework takes care of generating dynamic proxies to adapt the decorator on to any component in the system. My issue with this solution is the heavy reliance on reflection. Relying on reflection in the heart of my system seemed like a performance minefield.
Lack of Clarity
The above issues are not unique to C#. They exist in most statically-typed OO languages because there isn’t a great mechanism for generically passing along info.
I was facing a deeper issue though. I lacked clarity about how to practically use AOP. My expectations were to write decorators for all my components at once, but also to be able to access specific properties.
Authorization is a good example. Authorization often depends on some identifying information about the user. A decorator could grab the user id from the arguments and then make a decision based on that info. Sounds good, but how does it do that generally for any function that takes a user id? It requires name or type-based reflection (which would not be great security practice). The code can’t interpret your intended semantics without a consistent convention or annotation.
Functional Composition
I’d pretty much given up on AOP when I started to learn functional programming. Then, I witnessed composition in F#.
In functional languages
- functions are transformations from input to output, not an algorithm or set of instructions
- the transformation is referentially immutable, it should always return the same output for given input
- functions are data too. They can be assigned and operated on
- To operate on functions normally, functions with the same type signatures are implicitly interchangeable (i.e. you may have
sprint
in mind but specifying anint -> string
will accept any functionint -> string
) - arguments can be determined implicitly. For example, binding a function to a new name will implicitly impart arguments to the new function
1
let nya = (+) // nya takes two ints and outputs an int
- To operate on functions normally, functions with the same type signatures are implicitly interchangeable (i.e. you may have
This adds up for some powerful consequences. It means that the decorator pattern is a fundamental expected activity in functional languages. It is a sub-set of the functional composition and it even has an operator.
Here’s the core idea, if a function is a transformation that always produces that same thing, then why not connect multiple transformations to make a new transformation?
|
|
Reach back in your math education memories. This the functional equivalent of $F \circ G$. Most probably remember it as FoG and GoF.
Any two functions can be composed as long as the output of one matches the input of the other! This means we can easily create decorators that intercept, then pass on arguments or outputs.
|
|
Note that it does not matter what input is here. It is implicitly typed and will be decided based on where it is used!
This is a stark contrast to decorator in OO that requires a new type definition, constructor injection, and call forwarding of every method on the interface. This is much lighter weight.
Gaining Clarity
The functional AOP implementation is very much lighter and nicer than the OO one. However, I haven’t gained any abilities not available to me in C#. Well… I suppose I gained generic decorators with built-in language functionality.
This doesn’t resolve the generic decorators vs specific data conundrum.
The ease of implementation did, however, make it much easier to play and test my thoughts. I ended up realizing that there is a fundamental divide in AOP-style decorators.
The first class is completely generic. They cannot depend on specifics of the functions that they are modifying. This class of decorators is great for centralizing tasks like performance tracking, default authentication behavior, or error logging. Some crafty dependency injection can also achieve this for role- and component-based authentication.
The second class relies on some specific information of the function being decorated. There is not, and never was, a way to get around creating a custom implementation or configuration for this scenario. An example would be authenticating a method call based on current user and id of the entity being acted on, which is passed as an argument.
This is not a reason to be dismayed. The completely generic decorators are already a significant win. The specific decorators also end up small and focused. They’re often require similar effort as baking the concern into the core logic, but result in much more flexibility for change. In fact, the decorators can be decided at configuration time, where a baked-in approach can only be decided at write time.
Summary
AOP and decorators are definitely possible and beneficial in OO, but much more work. The functional focus on transformations and composition makes AOP both natural and simple to implement.
The simplicity of AOP in F# helped me to finally wrap my head around the paradigm as a practical tool.
Further Reading
- A nice exploration of the AOP options in C#. I did not cover them all here because there are a lot https://www.dotnetcurry.com/patterns-practices/1305/aspect-oriented-programming-aop-csharp-using-solid
- Composition as decorator https://fsharpforfunandprofit.com/posts/conciseness-functions-as-building-blocks/
- Mapping OO patterns to FP https://fsharpforfunandprofit.com/fppatterns/