Aspect-Oriented Programming Enables Better Code Encapsulatio

来源:岁月联盟 编辑:zhu 时间:2003-07-12
To wrap our target component for use by the client, we performed the following four steps:

We created the actual component using the CLSID of the component passed to the moniker earlier in the metadata XML file.

We created a DelegatorHook object for intercepting the QueryInterface calls to the object. The hook is responsible for routing method calls through each aspect.

Next, we created the UD object and retrieved the IDelegatorFactory interface.

Using IDelegatorFactory, we called CreateDelegator, passing the interface of the actual object, the delegator hook, the IID of the interface the original caller requested (riidResult), and the pointer to the interface pointer (ppvResult). The delegator returns a pointer to the interceptor that calls our delegator hook on each call.


Figure 7 COM AOP Architecture

The result is shown in Figure 7. From this point on, the client can use the interceptor as the actual interface pointer from the target component. As calls are made, they are routed through the aspects on the way to the target component.

Aspect Builder
To activate the component with all of the aspects strung together properly, our AOP moniker relies on an XML file to describe the component and the associated aspects. The simple format merely contains the CLSID of the component and the CLSIDs of the aspect components. Figure 8 shows an example that wraps the Microsoft FlexGrid Control with two aspects. In order to ease the task of creating instances of AOP metadata, we created Aspect Builder (as shown in Figure 9).


Figure 9 Aspect Builder

Aspect Builder enumerates all the aspects registered on the machine and displays each one of them as cloud-shaped items in the list view on the left. The client area of the Aspect Builder contains a visual representation of the component. You can double-click on it (or use the corresponding menu item) and specify the component's ProgID. Once you've chosen a component, you can drag and drop the aspects into the client area, adding aspects to your component's AOP metadata.
To produce the XML format necessary to feed to the AOP moniker, choose Compile from the Tools menu and the metadata will be shown in the bottom pane. You can verify that the metadata is indeed correct by doing the actual scripting in the Verify Aspects pane. You can save these compiled XML instances to the disk and reload them with Aspect Builder as well.

Aspects in .NET
While the Aspect Builder makes things very pretty, storing aspect metadata separately from the component makes programming AOP in COM less convenient than it could be. Unfortunately, COM's metadata leaves quite a bit to be desired when it comes to extensibility, which is why we felt the need to separate the metadata from the class in the first place. However, .NET, the heir apparent to COM, has no such issue. .NET's metadata is fully extensible and therefore has all the necessary underpinnings for associating aspects directly with the class itself via attributes. For example, given a custom .NET attribute, we can readily associate a call-tracing attribute with a .NET method:

public class Bar {
    [CallTracingAttribute("In Bar ctor")]
    public Bar() {}
    
    [CallTracingAttribute("In Bar.Calculate method")]
    public int Calculate(int x, int y){ return x + y; }
  }

Notice the square brackets containing the CallTracingAttribute and the string to be output when the method is accessed. This is the attribute syntax that associates the custom metadata with the two methods of Bar.
Like our AOP framework in COM, attributes in .NET are classes by components in .NET. A custom attribute in .NET is implemented with a class that derives from Attribute, as shown here:

  using System;
  using System.Reflection;

  [AttributeUsage( AttributeTargets.ClassMembers,
   AllowMultiple = false )]
  public class CallTracingAttribute : Attribute {    
      public CallTracingAttribute(string s) {
        Console.WriteLine(s);    
      }
  }

Our attribute class itself has attributes, which modify its behavior. In this case, we're requiring that the attribute be associated only with methods, and not assemblies, classes, or fields, and that each method may have only one trace attribute associated with it.
Once you've associated the attribute with the method, you're halfway home. To provide for AOP, you also need access to the call stack before and after each method to establish the environment necessary for the component to execute. This requires an interceptor and a context in which the component can live. In COM, we did this by requiring the client to activate using our AOP moniker. Luckily, .NET has built-in hooks that don't require the client to do anything special at all.

Context-bound Objects
The key to interception in .NET¡ªas in COM¡ªis to provide for a context for the COM component. In COM+ and in our custom AOP framework, we provided that context by stacking aspects between the client and the object to establish the properties of the context for the component before the method was executed. In .NET, a context is provided for any class that derives from System.ContextBoundObject:

public class LikeToLiveAlone : ContextBoundObject {...}

When an instance of the LikeToLiveAlone class is activated, the .NET runtime will automatically create a separate context for it to live in, establishing an interceptor from which to hang our own aspects. The .NET interceptor is a combination of two objects, the transparent proxy and the real proxy. The transparent proxy acts just like the target object, as our COM AOP interceptor did, and serializes the call stack into an object called a message, which it hands to the real proxy. The real proxy takes the message and sends it to the first message sink to process. The first message sink pre-processes the message, sends it along to the next message sink in the stack of message sinks between client and object, and then post-processes the message. The next message sink does the same, and so on until the stack builder sink is reached, which deserializes the message back into a call stack, calls the object, serializes the outbound parameters and the return value, and returns to the previous message sink. This call chain is shown in Figure 10.


Figure 10 Interception

To participate in this chain of message sinks we first need to update our attribute to participate with context-bound objects by deriving our attribute from ContextAttribute (instead of just Attribute) and by providing something called a context property:

[AttributeUsage(AttributeTargets.Class)]
public class CallTracingAttribute : ContextAttribute {
    public CallTracingAttribute() :
    base("CallTrace") {}
    public override void   
    GetPropertiesForNewContext
        (IConstructionCallMessage ccm) {
        ccm.ContextProperties.Add(new
        CallTracingProperty());
    }
    ???
}

When the object is activated, the GetPropertiesForNewContext method is called for each context attribute. This allows us to add our own context property to the list of properties associated with the new context that's being created for our object. A context property allows us to associate a message sink with an object in the chain of message sinks. The property class acts as a factory for our aspect message sinks by implementing IContextObject and IContextObjectSink:

public class CallTracingProperty : IContextProperty,
    IContributeObjectSink {
    public IMessageSink GetObjectSink(MarshalByRefObject o,
        IMessageSink next) {
        return new CallTracingAspect(next);
    }
    ???
}

The process of proxy-creating the attribute, which creates the context property, which then creates the message sink, is shown in Figure 11.


Figure 11 .NET MessageSink Creation

.NET Aspects
Once everything is attached properly, each call enters our aspect's implementation of IMessageSink. SyncProcessMessage, which allows us to pre- and post-process the message, is shown in Figure 12.
Finally, the context-bound class that wants to associate itself with the call-¡ª¡ªtracing aspect declares its preference using the CallTracingAttribute:

[AOP.Experiments.CallTracingAttribute()]
public class TraceMe : ContextBoundObject {
    public int ReturnFive(String s) {
        return 5;
    }
}

Notice that we're associating our context attribute with a class and not with each method. The .NET context architecture will automatically notify us on each method, so our call-¡ªtracing attribute has all of the information it needs, saving us the trouble of manually associating our attribute with each method, as we did with our plain attribute before. When the client class instantiates the class and calls a method, the aspect comes to life:

public class client {
    public static void Main() {
        TraceMe traceMe = new TraceMe();
        traceMe.ReturnFive("stringArg");
    }
}

When they're run, our client and aspect-oriented object output the following:

PreProcessing: TraceMe.ReturnFive(s= stringArg)
PostProcessing: TraceMe.ReturnFive( returned [5])

Aspect and Context
So far, our simple aspect hasn't really lived up to the intended ideal of AOP. While it's true that aspects can be utilized for pre- and post-processing of method calls alone, what's really interesting is how an aspect affects the execution of the method itself. For example, the COM+ transactional aspect causes all resource providers that are used during the object's method to participate in the same transaction, allowing the method to abort all activities by merely aborting the transaction provided by the COM+ transactional aspect. To allow for this, COM+ aspects augment the COM call context, which provides a rallying point for all components interested in accessing the current transaction. Likewise, .NET provides an extensible call context that we can make use of to allow a method. For example, we can allow an object wrapped in our call-tracing aspect to have the ability to add things to the stream of trace messages by putting ourselves into the .NET context, as you can see here:

internal class CallTracingAspect : IMessageSink {
    public static string ContextName {
        get { return "CallTrace" ; }
    }
    
    private void Preprocess(IMessage msg) {
        ???
        // set us up in the call context
        call.LogicalCallContext.SetData(ContextName, this);
    }
    ???
}

Once we've added the aspect to the call context, the method can pull us out again and participate in the tracing:

[CallTracingAttribute()]
public class TraceMe : ContextBoundObject {
    public int ReturnFive(String s) {
        Object obj =
            CallContext.GetData(CallTracingAspect.ContextName) ;
        CallTracingAspect aspect = (CallTracingAspect)obj ;
        aspect.Trace("Inside MethodCall");
        return 5;
    }

By providing a way to augment the call context, .NET allows aspects to set up a real environment for the objects. In our example, the object is allowed to add tracing statements to the stream without any need to know where the stream is going, how the stream is established, or when it will be torn down, much like the COM+ transactional aspect, as shown here:

PreProcessing: TraceMe.ReturnFive(s= stringArg)
During: TraceMe.ReturnFive: Inside MethodCall
PostProcessing: TraceMe.ReturnFive( returned [5])

Wrap-up
Aspect-oriented programming allows developers to encapsulate usage of common services across components in the same way that you encapsulate the components themselves. By using metadata and interceptors, you can stack arbitrary services between the client and the object, semi-seamlessly in COM and seamlessly in .NET. The aspects we've shown in this article have access to call stacks on the way into and out of method calls and they provide an augmented context in which our objects live. While still in its infancy when compared to structured programming or object-oriented programming, native support for AOP in .NET provides a valuable tool for pursuing Lego-like software fantasies.