This Series
- Getting Started with SheepAOP
- Pointcuts and SAQL Basics
- Aspects Lifecycles & Instantiations
- Integrating with IoC containers
- Aspects Inheritance & Polymorphism
- Attributive Aspects (ala PostSharp)
- Unit-testing your aspects
- Extending SheepAop
Reusing Advices
Think back to the concurrency example from our previous post. We used the ReadWriteLockAspect to define the reading and writing operations within our ShoppingCart, and apply a read/write locking pattern onto it. Surely we can apply this same pattern to other classes where a safe concurrent access is required. We can refactor this well-tested concurrency pattern into an abstract aspect that we can reuse instead of reimplementing it when we need it again.
This gives us a perfect situation to demonstrate SheepAop’s modularity and reuse, as we refactor our ReadWriteLockAspect example into the following.
[AspectPerThis("ReadingsPointcut", "WritingsPointcut")]
public abstract class ReadWriteLockAspect
{
protected abstract void ReadingsPointcut(); // Abstract pointcut
protected abstract void WritingsPointcut(); // Abstract pointcut
protected abstract int GetTimeout(); // Abstract method
private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
[Around("ReadingsPointcut")]
public object LockRead(JoinPoint jp)
{
try
{
_lock.EnterReadLock(GetTimeout());
return jp.Execute();
}
finally()
{
_lock.ExitReadLock();
}
}
[Around("WritingsPointcut")]
public void LockWrite(JoinPoint jp)
{
try
{
_lock.EnterWriteLock(GetTimeout());
jp.Execute();
}
finally()
{
_lock.ExitWriteLock();
}
}
}
Now to apply a write/lock behavior to our ShoppingCart class, we simply extend the aspect. We just need to provide the implementation of the pointcuts. The behavior of the advice will be inheritted from the base aspect.
public class ShoppingCartLockAspect: ReadWriteLockAspect
{
[SelectMethod("'* ShoppingCart::Get*(*)'")]
[SelectPropertyGet("'* ShoppingCart::*'")]
protected override void ReadingsPointcut(){}
[SelectMethod("InType:'ShoppingCart' && Name:'AddProduct'|'RemoveProduct'")]
protected override void WritingsPointcut(){}
protected override int GetTimeout()
{
return 2000; // Timeout is 2 seconds
}
}
Now we have used the base ReadWriteLockAspect to transform the ShoppingCart class to be thread-safe. We can reuse this same base aspect to any other target class that you intend to make thread-safe.
Tips on Pointcut Compositions: make sure you take advantage of the composability of SheepAop pointcuts when designing a reusable aspect. You can devide your pointcut into fragments to extract any reusable pattern out, and only expose a small fraction of it as abstract. As an example, our ReadWriteLockAspect can be modified to automatically apply the locks on all read/write methods and properties on the particular type that you target.
[AspectPerThis("ReadingsPointcut", "WritingsPointcut")]
public abstract class ReadWriteLockAspect
{
[SelectMethod("'InType:@TargetClass && Name:'Get*'|'Is*' && !ReturnsVoid")]
[SelectPropertyGet("InType:@TargetClass")]
protected void ReadingsPointcut(){}
[SelectMethod("InType:@TargetClass && Name:'Add*'|'Remove*'|'Set*'")]
[SelectPropertySet("InType:@TargetClass")]
protected void WritingsPointcut(){}
//This is a small fragment of the pointcut that we expose as abstract
protected abstract @TargetClass();
/* and all the rest of it unchanged.. */
}
Now our base aspect provides a reusable template for our pointcuts, where a small fragment @TargetClass is left out as abstract to be composed to form the complete pointcut. The concrete aspect only needs to provide the implementation of this small fragment, rather than the whole pointcut expressions.
public class ShoppingCartLockAspect: ReadWriteLockAspect
{
[SelectTypes("'ShoppingCart'")]
protected override void TargetClass(){}
protected override int GetTimeout()
{
return 2000; // Timeout is 2 seconds
}
}
Other example
The ActorAspect example can also be refactored into an abstract class to reuse its advice behavior, but I will leave that as an exercise for the reader.
Reusing Pointcuts
In the previous example, we have used aspect inheritance to reuse a common advice behavior (read/write locking behavior) repeatedly for multiple pointcuts. Aspect inheritance can also be used to reuse common pointcut expressions repeatedly in multiple advice implementations. A common example of this is aspects for defining extensibility points in a plugin architecture.
For instance, consider you want to define an extensibility point that is triggered at the point of every bank-account transaction. So we define the following abstract aspect.
[Aspect]
public abstract class AccountTransactionPlugin
{
[SelectMethod(@"InType:AssignableTo:'AccountBase'
&& Name: 'Withdraw'|'Deposit'
&& ArgTypes:'Money'")]
protected void TransactionPointcut(){}
[Around("TransactionPointcut")]
public void AroundTransaction(MethodJointPoint jp)
{
OnTransacting((Account)jp.This, (Money)jp.Args[0], ()=> jp.Execute());
}
protected abstract OnTransacting(Account account, Money amount, Action proceed);
}
Now that we have this extensibility point defined, a third party can provide their plugins by implementing this base-class. For example, the following plugin will add a business policy to limit transaction amounts within our banking system.
public class TransactionLimitPlugin: AccountTransactionPlugin
{
private const int CompanyLimit = 100000;
private const int PersonalLimit = 2000;
protected override OnTransacting(Account account, Money amount, Account proceed)
{
var limit = account.Customer.IsCompany? CompanyLimit: PersonalLimit;
if(amount > limit)
throw new TransactionOverlimitException(amount, limit);
proceed();
}
}
More plugins for this specific extensibility point can be added to the system by writing more implementations of AccountTransactionPlugin (e.g. a plugin to apply transaction-fees depending on the types of the account). Typically we would have a set these abstract aspects with different pointcut-expressions, each represents different extensibility point within the system (e.g. when an order is created, when a case is raised, when a conversation occurs, etc), onto which a specific plugin can be hooked and unhooked. You can jazz up this technique by adding MEF to the mix.
Note that the domain code of our application might not be designed with plugin architecture in mind. It probably does not even aware of any extensibility system that is in place. This brings us to our next point…
Speaking of Extensibility..
Extensibility has always been a chicken-and-egg problem. Many business applications require the extensibility to accomodate rapidly changing business rules, policies, and workflows, sometimes allowing extensibility by third-party developers. To achieve this, architects are often faced with underdesign/overdesign issue. If you underdesign, you may have to make massive changes later in the development cycle. If you overdesign with excessive amount of extensibility point on every method of your application, the implementation may be burdened with code of questionable usefulness.
With AOP, you can delay making design decisions for future requirements because you can implement those extensibility points unobtrusively using aspects. You can focus on the current requirement of the system.
In this respect, AOP works in harmony with YAGNI (You aren’t gonna need it). Implementing a feature just because you may need it in the future often results in wasted effort because you won’t actually need it. With AOP, you can stick faithfully to your current requirement, and if you do need a particular kind of functionality later, you can implement it without having to make system-wide modifications
Attribute-based Aspects (ala Postsharp)
(Since this question gets asked quite a lot, I will post this section again in a separate post as a handy reference)
SheepAop is not specifically built to support attribute-based declarative approach used in Postsharp. But using the rich pointcut expressions and aspect inheritance in SheepAop, it’s quite trivial to implement our own implementation. We just simply need to write this simple abstract aspect to deliver the same attribute-based functionality.
[Aspect]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public abstract class AroundMemberAttribute: Attribute
{
[SelectMethods("HasCustomAttributeType:ThisAspect")]
[SelectPropertyGets("HasCustomAttributeType:ThisAspect")]
[SelectPropertySets("HasCustomAttributeType:ThisAspect")]
protected void DecoratedPointcut(){}
[Around("DecoratedPointcut")]
public abstract object Around(JointPoint jp);
}
This aspect simply defines a pointcut that matches all methods and properties that are decorated using the concrete type of this aspect. So now we can use this base attribute to create our TransactionalAttribute as an example.
[Aspect]
public class TransactionalAttribute: AroundMemberAttribute
{
public override object Around(JointPoint jp)
{
using(var tx = new TransactionScope())
{
var result = jp.Execute();
tx.Complete();
return result;
}
}
}
That is all it. Now you can use this attribute to decorate methods and properties that you want to make transactional. For instance:
[Aspect]
public class OrderService: IOrderService
{
[Transactional]
public void ProvisionOrder(Order order)
{
// .... whatever ...
}
}
For your convenience, the base-aspect AroundMemberAttribute is included straight out-of-the-box within SheepAop, so you don’t have to write it every time. Another variation of the base attribute is called AroundCallAttribute. They both have equivalent functionality, but whereas AroundMemberAttributes targets the bodies of methods and properties decorated with the attribute, AroundCallAttribute targets the calling of methods, getting/setting properties, and reading/writing fields decorated with the attribute.
Summary
Inheritance is a key component in making reusable aspects in SheepAop. Since SheepAop aspects are just normal POCO classes, you can make use of the same Object-Oriented techniques such as inheritance and polymorphism that we’re all familiar with. The same techniques are also used to implement the declarative attribute-based aspects that gives a lot of convenience for many simple cases.