Castle Dynamic Proxy tutorial part IV: breaking hard dependencies

This is part four of my tutorial on Castle Dynamic Proxy.

In the last part of the tutorial we created a method GetInterceptedMethodsCountFor that I promised I’ll talk about soon. While we’re at it, we’re going to fix another design flaw of our Freezable class.

To do its work, it holds a hard reference to each and every freezable object it creates. This is obviously not a big deal if you create only a handful of freezable objects that are we want to be alive for the entire time the application is running.

private static readonly IDictionary<object, IFreezable> _freezables = new Dictionary<object, IFreezable>();

However, in most cases the objects we create are transient, and we create a lot of them. Looking at it from this perspective – we have a memory leak. Freezable class holds a reference to the objects, so garbage collector can not collect them and reclaim the memory they occupy, even though we may not use these objects ever again.

So how do we fix it? For now we’ll use evil hack I introduced in GetInterceptedMethodsCountFor method in the last part. Ultimately, we’re arrive at much nicer solution when we talk about mixins. But let’s not get ahead of ourselves.

If you took a look at the code from the previous part, the method is implemented as follows:

private int GetInterceptedMethodsCountFor(object freezable)
{
    Assert.True(Freezable.IsFreezable(freezable));
 
    var hack = freezable as IProxyTargetAccessor;
    Assert.NotNull(hack);
    var loggingInterceptor = hack.GetInterceptors().
                                 Where(i => i is CallLoggingInterceptor).
                                 Single() as CallLoggingInterceptor;
    return loggingInterceptor.Count;
}

The evil hack is based on the fact, that each proxy generated by Castle Dynamic Proxy framework implements an IProxyTargetAccessor interface. If you take a look at how it looks, it has two getter methods:

dptutorial_4_IProxyTargetAccessor

DynProxyGetTarget returns the proxies object. In our case the proxied object is the proxy itself, as we can prove with the following test

[Fact]
public void DynProxyGetTarget_should_return_proxy_itself()
{
    var pet = Freezable.MakeFreezable<Pet>();
    var hack = pet as IProxyTargetAccessor;
    Assert.NotNull(hack);
    Assert.Same(pet, hack.DynProxyGetTarget());
}

This is logical if you consider how DynamicProxy creates class proxies. It does so by creating a subclass of proxied type. The whole interception magic happens in overridden virtual methods and the methods of actual intercepted type are called via base.MyMethod(args). That’s why IProxyGenerationHook contains a method that allows you to act upon methods that are not virtual and hence can not be intercepted. We’ll use this feature in forthcoming part of the tutorial.

The other method, GetInterceptors, returns the interceptors associated with given proxy. We can use this method to obtain the proxy interceptors without keeping any hard reference to it.

So out task for this part of the tutorial is to break the need for hard reference the Freezable class holds to freezable objects. We state the requirement with the following test:

[Fact]
public void Freezable_should_not_hold_any_reference_to_created_objects()
{
    var pet = Freezable.MakeFreezable<Pet>();
    var petWeakReference = new WeakReference(pet, false);
    pet = null;
    GC.Collect();
    Assert.False(petWeakReference.IsAlive, "Object should have been collected");
}

If you run this test now, it will fail (go on, see for yourself, don’t get my word on it).

Now let’s factor out the dependency with out new found tool. First we refactor IsFreezable method, to the following.

public static bool IsFreezable(object obj)
{
    if (obj == null)
        return false;
    var hack = obj as IProxyTargetAccessor;
    if (hack == null)
        return false;
    return hack.GetInterceptors().Count(i => i is IFreezable) > 0;
}

If we run out tests, the old tests will pass (great, we didn’t break anything), but the new test, will still fail, as we have two more methods to refactor. Let’s now go to the IsFrozen method. I’m going to cheat here however. While implementing the method, I noticed that there’s a portion of code that each method in the class will require, so I factored it out, to another method. With that, here’s changed code:

public static bool IsFreezable(object obj)
{
    return AsFreezable(obj) != null;
}
 
private static IFreezable AsFreezable(object target)
{
    if (target == null)
        return null;
    var hack = target as IProxyTargetAccessor;
    if (hack == null)
        return null;
    return hack.GetInterceptors().FirstOrDefault(i => i is FreezableInterceptor) as IFreezable;
}
 
public static bool IsFrozen(object obj)
{
    var freezable = AsFreezable(obj);
    return freezable != null && freezable.IsFrozen;
}

We added the AsFreezable method that returns either IFreezable implementation associated with given object, or null, if there isn’t any.

We have two more methods to refactor:

public static void Freeze(object freezable)
{
    var interceptor = AsFreezable(freezable);
    if (interceptor == null)
        throw new NotFreezableObjectException(freezable);
    interceptor.Freeze();
}

Freeze implementation is still very simple. The MakeFreezable<TFreezable>() method only puts newly created objects into the dictionary, so we can safely remove it from that method. We can now also delete the dictionary field, as it’s not used anymore.

If you run the tests now, you’ll see that they all pass, including the newly added one.

dptutorial_4_tests_passed

The code, including tests, is here.

Final words

Even though we fixed the memory leak issue, it still is not the best solution. That said, IProxyTargetAccessor is a useful interface and it’s good to know that its there when you need it, but most of the time you don’t need it and in almost every case there’s a better way to accomplish your goal, without using the interface.

It’s mostly intended for use in low level, framework infrastructure code, and if used anywhere else you should treat it as a warning sign.

Technorati Tags: , ,

1 Thought.

Comments are closed.