4 - Interception
On this page: | Download: |
---|---|
Introduction | Crosscutting Concerns | The Decorator Pattern | Using Unity to Wire Up Decorator Chains | Aspect Oriented Programming | Interception | Instance Interception | Type Interception | Summary |
Introduction
In Chapter 1, you learned about some of the motivations for adopting a loosely coupled design, and in the following chapters, you saw how dependency injection (DI) and the Unity container can help you to realize these benefits in your own applications. One of the motivations that Chapter 1 described for adopting a loosely coupled design was crosscutting concerns. Although DI enables you to inject dependencies when you instantiate the objects that your application will use at run time and helps you to ensure that the objects that Unity instantiates on your behalf address your crosscutting concerns, you will still want to try and follow the single responsibility and open/closed principles in your design. For example, the classes that implement your business behaviors should not also be responsible for logging or validation, and you should not need to modify your existing classes when you add support for a new crosscutting concern to your application. Interception will help you to separate out the logic that handles cross-cutting concerns from your LOB classes.
Crosscutting Concerns
Crosscutting concerns are concerns that affect many areas of your application. For example, in a LOB application, you may have a requirement to write information to a log file from many different areas in your application. The term crosscutting concerns, refers to the fact that these types of concern typically cut across your application and do not align with the modules, inheritance hierarchies, and namespaces that help you to structure your application in ways that align with your application’s business domain. Common crosscutting concerns for LOB applications include:
- Logging. Writing diagnostic messages to a log for troubleshooting, tracing, or auditing purposes.
- Validation. Checking that the input from users or other systems complies with a set of rules.
- Exception handling. Using a common approach to exception handling in the application.
- Transient fault handling. Using a common approach to identifying transient faults and retrying operations.
- Authentication and authorization. Identifying the caller and determining if that caller should be allowed to perform an operation.
- Caching. Caching frequently used objects and resources to improve performance.
- Performance monitoring. Collecting performance data in order to measure SLA compliance.
- Encryption. Using a common service to encrypt and decrypt messages within the application.
- Mapping. Providing a mapping or translation service for data as it moves between classes or components.
- Compression. Providing a service to compress data as it moves between classes or components.
It’s possible that many different classes and components within your application will need to implement some of these behaviors (Unity often uses the term behaviors to refer to the logic that implements cross-cutting concerns in your code). However, implementing support for these crosscutting concerns in a LOB application introduces a number of challenges such as how you can:
- Maintain consistency. You want to be sure that all the classes and components that need to implement one of these behaviors do so in a consistent way. Also, if you need to modify the way that your application supports one of these crosscutting concerns, then you want to be sure that the change is applied everywhere.
- Create maintainable code. The single responsibility principle helps to make your code more maintainable. A class that implements a piece of business functionality should not also be responsible for implementing a crosscutting concern such as logging.
- Avoid duplicate code. You don’t want to have the same code duplicated in multiple locations within your application.
Before examining how interception and Unity’s implementation of interception can help you to address cross-cutting concerns in your applications, it’s worth examining some alternative approaches. The decorator pattern offers an approach that doesn’t require a container and that you can implement yourself without any dependencies on special frameworks or class libraries. Aspect oriented programming (AOP) is another approach that adopts a different paradigm for addressing cross-cutting concerns.
The Decorator Pattern
A common approach to implementing behaviors to address crosscutting concerns in your application is to use the decorator pattern. The basis of the decorator pattern is to create wrappers that handle the crosscutting concerns around your existing objects. In previous chapters, you saw the TenantStore class that is responsible for retrieving tenant information from, and saving tenant information to a data store. You will now see how you can use the decorator pattern to add logic to handle crosscutting concerns such as logging and caching without modifying or extending the existing TenantStore class.
Carlos says: | |
---|---|
If you extended the TenantStore class to add support for logging you would be adding an additional responsibility to an existing class, breaking the single responsibility principle. |
The following code sample shows the existing TenantStore class and ITenantStore interface.
public interface ITenantStore
{
void Initialize();
Tenant GetTenant(string tenant);
IEnumerable<string> GetTenantNames();
void SaveTenant(Tenant tenant);
void UploadLogo(string tenant, byte[] logo);
}
public class TenantStore : ITenantStore
{
...
public TenantStore(IBlobContainer<Tenant> tenantBlobContainer,
IBlobContainer<byte[]> logosBlobContainer)
{
...
}
...
}
The following code sample shows a decorator class that adds logging functionality to the existing TenantStore class:
class LoggingTenantStore : ITenantStore
{
private readonly ITenantStore tenantStore;
private readonly ILogger logger;
public LoggingTenantStore(ITenantStore tenantstore, ILogger logger)
{
this.tenantStore = tenantstore;
this.logger = logger;
}
public void Initialize()
{
tenantStore.Initialize();
}
public Tenant GetTenant(string tenant)
{
return tenantStore.GetTenant(tenant);
}
public IEnumerable<string> GetTenantNames()
{
return tenantStore.GetTenantNames();
}
public void SaveTenant(Tenant tenant)
{
tenantStore.SaveTenant(tenant);
logger.WriteLogMessage("Saved tenant" + tenant.Name);
}
public void UploadLogo(string tenant, byte[] logo)
{
tenantStore.UploadLogo(tenant, logo);
logger.WriteLogMessage("Uploaded logo for " + tenant);
}
}
Note how this decorator class also implements the ITenantStore interface and accepts an ITenantStore instance as a parameter to the constructor. In each method body, it invokes the original method before it performs any necessary work related to logging. You could also reverse this order and perform the work that relates to the crosscutting concern before you invoke the original method. You could also perform work related to the crosscutting concern both before and after invoking the original method.
If you had another decorator class called CachingTenantStore that added caching behavior you could create an ITenantStore instance that also handles logging and caching using the following code.
var basicTenantStore = new TenantStore(tenantContainer, blobContainer);
var loggingTenantStore = new LoggingTenantStore(basicTenantStore, logger);
var cachingAndLoggingTenantStore = new CachingTenantStore(loggingTenantStore, cache);
If you invoke the UploadLogo method on the cachingAndLoggingTenantStore object, then you will first invoke the UploadLogo method in the CachingTenantStore class that will in turn invoke the UploadLogo method in the LoggingTenantStore class, which will in turn invoke the original UploadLogo method in the TenantStore class before returning through the sequence of decorators.
Figure 1 shows the relationships between the various objects at run time after you have instantiated the classes. You can see how each UploadLogo method performs its crosscutting concern functionality before it invokes the next decorator in the chain, until it gets to the end of the chain and invokes the original UploadLogo method on the TenantStore instance.
Figure 1 - The decorator pattern at run time
Using Unity to Wire Up Decorator Chains
Instead of wiring the decorators up manually, you could use the Unity container to do it for you. The following set of registrations will result in the same chain of decorators shown in figure 1 when you resolve the default ITenantStore type.
container.RegisterType<ILogger, Logger>();
container.RegisterType<ICacheManager, SimpleCache>();
container.RegisterType<ITenantStore, TenantStore>(
"BasicStore");
container.RegisterType<ITenantStore, LoggingTenantStore>(
"LoggingStore",
new InjectionConstructor(
new ResolvedParameter<ITenantStore>("BasicStore"),
new ResolvedParameter<ILogger>()));
// Default registration
container.RegisterType<ITenantStore, CachingTenantStore>(
new InjectionConstructor(
new ResolvedParameter<ITenantStore>("LoggingStore"),
new ResolvedParameter<ICacheManager>()));
Aspect Oriented Programming
Aspect Oriented Programming (AOP) is closely related to the issue of handling crosscutting concerns in your application. In AOP, you have some classes in your application that handle the core business logic and other classes that handle aspects or crosscutting concerns. In the example shown in the previous section, the TenantStore class is responsible for some of the business logic in your LOB application, and the classes that implement the ILogger and ICacheManager interfaces are responsible for handling the aspects (or crosscutting concerns) in the application.
The previous example shows how you can use the decorator pattern to explicitly wire-up the classes responsible for the crosscutting concerns. While this approach works, it does require you write the decorator classes (CachingTenantStore and LoggingTenantStore) in addition to wiring everything together, either explicitly or using DI.
AOP is a mechanism that is intended to simplify how you wire-up the classes that are responsible for the crosscutting concerns to the business classes: there should be no need to write the decorator classes and you should be able to easily attach the aspects that handle the crosscutting concerns to your standard classes. The way that AOP frameworks are implemented is often technology dependent because it requires a dynamic mechanism to wire-up the aspect classes to the business classes after they have all been compiled.
Note
For an example of an AOP framework for C#, see “What is PostSharp?” AspectJ is a widely used AOP framework for Java.
Interception
Interception is one approach to implementing the dynamic wire up that is necessary for AOP. You can use interception as a mechanism in its own right to insert code dynamically without necessarily adopting AOP, but interception is often used as the underlying mechanism in AOP approaches.
Jana says: | |
---|---|
The interception approach that Unity adopts is not, strictly speaking, an AOP approach, although it does have many similarities with true AOP approaches. For example, Unity interception only supports preprocessing and post-processing behaviors around method calls and does not insert code into the methods of the target object. Also, Unity interception does not support interception for class constructors. |
Interception works by dynamically inserting code (typically code that is responsible for crosscutting concerns) between the calling code and the target object. You can insert code before a method call so that it has access to the parameters being passed, or afterwards so that it has access to the return value or unhandled exceptions. This inserted code typically implements what are known as behaviors (behaviors typically implement support for cross-cutting concerns), and you can insert multiple behaviors into a pipeline between the client object and the target object in a similar way to using a chain of decorators in the decorator pattern to add support for multiple crosscutting concerns. The key difference between interception and the decorator pattern is that the interception framework dynamically creates decorator classes such as LoggingTenantStore and CachingTenantStore at run time. This makes it much easier to add behaviors that provide support for crosscutting concerns or aspects because you no longer need to manually create decorator classes for every business class that needs to support the behaviors. Now, you can use a configuration mechanism to associate the classes that implement the behaviors with the business classes that need the behaviors.
For a discussion of AOP and interception with Unity, see the article “Aspect-Oriented Programming, Interception and Unity 2.0” by Dino Esposito on MSDN.
There are many ways that you can implement interception, but the two approaches that Unity supports are known as instance interception and type interception. The next two sections describe these two different approaches.
Instance Interception
With instance interception, Unity dynamically creates a proxy object that it inserts between the client and the target object. The proxy object is responsible for passing the calls made by the client to the target object through the behaviors that are responsible for the crosscutting concerns.
Figure 2 shows how when you use instance interception, the Unity container instantiates the target TenantStore object, instantiates the pipeline of behaviors that implement the crosscutting concerns, and generates and instantiates a TenantStore proxy object.
Figure 2 - An example of instance interception
Instance interception is the more commonly used of the two interception techniques supported by Unity and you can use it if the target object either implements the MarshalByRefObject abstract class or implements a public interface that defines the methods that you need to intercept.
You can use Unity instance interception to intercept objects created both by the Unity container and outside of the container, and you can use instance interception to intercept both virtual and non-virtual methods. However, you cannot cast the dynamically created proxy type to the type of the target object.
Type Interception
With type interception, Unity dynamically creates a new type that derives from the type of the target object and that includes the behaviors that handle the crosscutting concerns. The Unity container instantiates objects of the derived type at run time.
Figure 3 shows how with type interception, the Unity container generates and instantiates an object of a type derived from the target TenantStore type that encapsulates the behavior pipeline in the overridden method calls.
Figure 3 - An example of type interception
You can only use Unity type interception to intercept virtual methods. However, because the new type derives from the original target type, you can use it wherever you used the original target type.
For more information about the differences between these approaches, see Unity Interception Techniques.
Summary
In this chapter, you learned how interception enables you to add support for crosscutting concerns to your application by intercepting calls to your business objects at run time. Interception uses dynamically created classes to implement the decorator pattern at run time. The next chapter describes in detail how you can implement interception using the Unity container.