I don't think you're missing anything here and the limitation is indeed the result of using compile time weaving.
Although I think compile time weaving tools have its place in software development, I feel that they are often overused. Often I see them being used to patch flaws in the application design. In the applications I build I apply generic interfaces to certain architectural concepts. For instance, I define:
- an
ICommandHandler<TCommand>
interface for services that implement a certain use case;
- an
IQueryHandler<TQuery, TResult>
interface for services that execute a query;
- an
IRepository<TEntity>
interface as abstraction over repositories;
- an
IValidator<TCommand>
interface for components that execute message validation;
- and so on, and so on.
This allows me to create a single generic decorator for such group of artifacts (for instance an TransactionCommandHandlerDecorator<TCommand>
that allows running each use case in its own transaction). The use of decorators has many advantages, such as:
- Those generic decorators are completely tool agnostic, since there is no reference to a code weaving tool or interception library. PostSharp aspects are completely dependent on PostSharp, and interceptors always take a dependency on an interception framework, such as Castle.DynamicProxy.
- Because a decorator is just a normal component, dependencies can be injected into the constructor and they can play a normal role when you compose your object graphs using Dependency Injection.
- The decorator code is very clean, since the lack of dependency with any third-party tool.
- Because they're tool agnostic and allow dependency injection, decorators can be unit tested easily without having to revert to special tricks.
- Application code that needs cross-cutting concerns to be applied can as well be tested easily in isolation, because decorators are not weaved in at compile time. When decorators are weaved in at compile time, you're always forced to do a integration style of testing of your application code, or need to revert to special build tricks to prevent them from being applied in your unit test project.
- Decorators can be applied dynamically and conditionally at runtime, since there's no compile time code weaving going on.
- Performance is identical (or even faster) than with code weaving, because there's no reflection going on during object construction.
- There's no need to mark your components with attributes to note that some aspect must be applied. This keeps your application code free of any knowledge of such cross-cutting concern and makes it much easier to replace this.
A lot has been written about this kind of application design; here are a few articles I wrote myself:
UPDATE
Decorators are great, but what I like about AOP is it's concept of
advice and join points. Is there a way to simulate the same capability
with decorator? I could only think of reflection right now.
A Join Point is a "well defined location within a class where a concern is going to be attached". When you apply AOP using decorators, you will be 'limited' to join points that are on the method boundaries. If however you adhere to the SRP, OCP and ISP, you will have very thin interfaces (usually with a single method). When doing that, you will notice that there is hardly ever a reason for having a join point at any other place in your classes.
An Advice is a "concern which will potentially change the input and/or output of the targeted method". When working with decorators and a message-based design (the thing I'm promoting here), your Advice needs to change the message (or replace the complete message with altered values) or change the output value. Things aren't that much different than with code weaving—if you apply an Advice, there must be something in common between all code the Advice is applied to.