Software Mechanics
Why do we even have that lever?

Deconstructing ObjectBuilder - Combining Strategies

February 2, 2008 19:09 by Chris

Combining Strategies

We’ve seen how ObjectBuilder can be used to construct objects. But for the simplistic case presented, the extra complexity doesn’t buy us anything. ObjectBuilder doesn’t come into its own until more complex object creation is needed. Multiple strategies can be combined into a single chain to implement arbitrarily sophisticated logic.

For example, it’s a fairly common requirement to cache objects. The first time you request something, you actually create it. Creating these objects can be expensive: reading from databases, calling web services, or whatnot. You don’t want to pay that creation cost every time, so you sock away a copy of the object, and the second and later requests use the already created copy. Let’s look at what it takes to build a simple, general purpose caching factory.

Identifying Objects to Build

Let’s start by writing a test that indicates what we want the API to look like for our new factory. We want to be able to create an object that we don’t already have, but we want to retrieve that exact same object instance the second time. Since we may have multiple Customer objects we wish to retrieve, we should be able to assign and get by an id. Our test looks like this:

1  [TestMethod]
2  public void ShouldGetCachedObjectSecondTime()
3  {
4    CachingFactory factory = new CachingFactory();
5    factory.SetCached<Customer>(true, "Jane");
6
7    Customer c1 = factory.Get<Customer>("Jane", "Jane", "Doe");
8    Customer c2 = factory.Get<Customer>("Jane");
9
10    Assert.AreSame(c1, c2);
11 }

Our factory will have a SetCached method to specify which types should be cached and which shouldn’t be, and the Get method will either create the object we want, or fetch the previously created one.

Note that both methods take a string parameter – this is the ID of the object we want to cache. Notice that the second call to Get doesn’t specify the constructor parameters, just the ID. As long as the ID’s are unique, it doesn’t really matter what the actual value is (it could even be null).

So that’s what we want the API to look like. What do we need to implement it? If you’re thinking strategies and policies, you’re right, but there’s two new pieces that needs to be introduced first.

Build Keys

So far, we've just passed an object's type as the buildKey parameter. Until now, that's all the information we needed, but not anymore. Luckily, build keys can be a lot more than just Type instances.

There's two static method on the BuilderStrategy class, GetTypeFromBuildKey and TryGetTypeFromBuildKey. Let's take a look at the latter:

1  public static bool TryGetTypeFromBuildKey(object buildKey, out Type type) {
2    type = buildKey as Type;
3
4    if(type == null) {
5        ITypeBasedBuildKey typeBasedBuildKey = buildKey as ITypeBasedBuildKey;
6        if(typeBasedBuildKey != null)
7        type = typeBasedBuildKey.Type;
8    }
9
10   return type != null;
11 }

(GetTypeFromBuildKey just calls TryGetTypeFromBuildKey and throws if it fails).

This method starts out with the basic case: if the build key object is a type, it simply returns it. However, if the build key is not a type, it still needs to get a type from it somehow. So it falls back onto the ITypeBasedBuildKey interface, defined as:

1  public interface ITypeBasedBuildKey
2  {
3    Type Type { get; }
4  }

This interface is trivial to implement, but provides a common hook for build keys so that OB can always get a type out.

We need to implement a build key that holds both the type, and the id we're requesting. This is a pretty simple little struct that implements ITypeBasedBuildKey:

1    struct TypeAndIDBuildKey : ITypeBasedBuildKey {
2        public Type TypeToBuild;
3        public string Id;
4
5        public TypeAndIDBuildKey(Type type, string id) {
6            TypeToBuild = type;
7            Id = id;
8        }
9        
10       public Type Type {
11           get { return TypeToBuild; }
12       }
13   }

It's actually important here that the build key is defined as a struct, not a class. As you'll see later, build keys generally have to have value semantics for comparison, which is automatic when using a struct.

Where’s the cache?

Our factory creates objects, but in order to return already existing objects, we’re going to need to hold onto the object references somewhere so we can look them up again later. Thankfully, ObjectBuilder provides a facility to do exactly that.

Let’s look back at the definition of (one of the overloads of) the the IBuilder.BuildUp method:

1  object BuildUp(
2    IReadWriteLocator locator,
3    ILifetimeContainer lifetime,
4    IPolicyList policies,
5    IStrategyChain strategies,
6    object buildKey,
7    object existing);

That first parameter, locator, is what does the trick. Until now, we’ve passed null for this parameter. Now let’s look at what an IReadWriteLocator can do for us. At it’s simplest, a locator is a dictionary. You put objects into it with a given key, and you can later get them back out again using that same key. The IReadWriteLocator interface (and its base interface, IReadableLocator) support a variety of methods to query the locator for its contents.

ObjectBuilder contains an implementation of the IReadWriteLocator interface named, oddly enough, Locator, which implements a weak-referenced dictionary. A weak reference is a reference to an object that does not prevent an object from being collected by the garbage collector. This is actually ideal for our cache; if we’re actively using a cached object, it’ll be available, but if memory pressure gets tight, the GC can clean up those objects that are only referenced by the Locator (i.e. not being used currently).

So, knowing that we need a locator, let’s take a first stab at implementing the CachingFactory:

1   public class CachingFactory {
2       private IReadWriteLocator cache;
3       private StagedStrategyChain<BuilderStage> strategies = 
4           new StagedStrategyChain<BuilderStage>();
5       private PolicyList policies = new PolicyList();
6
7       public CachingFactory() {
8           cache = new Locator();
9       }
10
11      public T Get<T>(string id, params object[] constructorParams) {
12          return (T)(new Builder().BuildUp(
13              cache,
14              null,
15              CreateConstructorParameterPolicy(
16              	typeof(T), id, constructorParams),
17              strategies.MakeStrategyChain(),
18              CreateKey<T>(id),
19              null));
20      }
21
22      public void SetCached<T>(bool shouldCache) {
23          SetCached<T>(shouldCache, null);    
24      }
25
26      private PolicyList 
27      CreateConstructorParameterPolicy(
28          Type typeToCreate, string id,
29          object[] parameters)
30      {
31          PolicyList policies = new PolicyList(this.policies);
32          policies.Set<ICreationParameterPolicy>(
33              new CreationParameterPolicy(parameters),
34              new TypeAndIDBuildKey(typeToCreate, id));
35          return policies;
36      }
37
38      private object CreateKey<T>(string id) {
39          return new TypeAndIDBuildKey(typeof (T), id);
40      }
41  }

We store the locator as a member variable, and pass it in on every call to BuildUp. We use the CreateKey helper method to create our build key, which combines the type and id into a single value that can be passed down to the strategies.

We’ve got the skeleton now, but our test still doesn’t run. In fact, it doesn’t even compile yet. We need to implement the SetCached method. Let’s do that next.

When we needed to pass constructor parameters to the strategies, we used a policy object. Those policies were transient, as we needed to pass different parameters every time. The caching settings, on the other hand, stick around across calls. So we need to build a persistent policy. The difference is trivial; we simply add the new policy to the member variable policy list instead of to the one we create every time.

Defining the policy is pretty simple. We saw in Chapter 1 that the PolicyList itself will map a policy object to a build key. This means our policy itself just needs to indicate if caching is on or off. Our caching policy interface looks like this:

1 	public interface ICachingPolicy : IBuilderPolicy
2 	{
3 	    bool ShouldCache { get; }
4 	}

I wrote two implementations of this interface:

5 	class ShouldCachePolicy : ICachingPolicy
6 	{
7 	    public bool ShouldCache
8 	    {
9 	        get { return true; }
10 	    }
11 	}
12 	
13 	class ShouldNotCachePolicy : ICachingPolicy
14 	{
15 	    public bool ShouldCache
16 	    {
17 	        get { return false; }
18 	    }
19 	}

With these classes in place, we can implement SetCached as follows:

1    public class CachingFactory
2    {
3        private IReadWriteLocator cache;
4        private StagedStrategyChain<BuilderStage> strategies = new StagedStrategyChain<BuilderStage>();
5        private PolicyList policies = new PolicyList();
6
7        private ICachingPolicy shouldCachePolicy = new ShouldCachePolicy();
8        private ICachingPolicy shouldNotCachePolicy = new ShouldNotCachePolicy();
9
10       ...
11
12       public void SetCached<T>(bool shouldCache, string id) {
13          ICachingPolicy cachingPolicy = shouldNotCachePolicy;
14          if(shouldCache) {
15              cachingPolicy = shouldCachePolicy;
16          }
17          policies.Set<ICachingPolicy>(cachingPolicy, CreateKey<T>(id));
18      }
19      
20      ...
21  }

The important line here is the call to Policies.Set on line 17. This sets the policy into the builder’s persistent policy list. This is automatically passed to the strategy chain on the call to BuildUp. We use the build key (which includes the type and id) to set the policy, so they can be looked up later.

Strategies, and the combination thereof

So now the builder can tell us if an object should be cached or not. Next, we need to add the strategies that actually implement the cache. The construction logic goes something like this:

  • If object should be cached:
    • If object is present in the locator, return it
    • Else:
      • Create the object
      • Store it in the locator
    • Return created object

We already implemented the “Create the object” step in Chapter 1, and I’d like to reuse that work. Let’s look at what’s required to do the look up and storage steps. We’ll implement these two steps as separate strategies. This makes sense, as the need to happen at different times in the pipeline. Looking the cached object up happens first, so let’s start there.

Our CacheRetrievalStrategy looks like this:

1   class CacheRetrievalStrategy : BuilderStrategy {
2       public override object BuildUp(
3       	IBuilderContext context, 
4       	object buildKey, 
5       	object existing)
6       {
7           ICachingPolicy cachePolicy = 
8               context.Policies.Get<ICachingPolicy>(buildKey);
9               
10          if(cachePolicy != null ) {
11              if(cachePolicy.ShouldCache) {
12                  object cached = context.Locator.Get(buildKey);
13                  if(cached != null) {
14                     return cached;
15                  }
16              }
17          }
18          return base.BuildUp(context, buildKey, existing);
19      }
20  }

Let’s walk though the implementation.

Lines 7-8 retrieve the cache policy for the currently requested build key. Not having a caching policy (if context.Policies.Get returns null) is the same as saying “don’t cache”. If we do have a caching policy, and the policy says to cache (lines 10-11) we need to look up the object in the locator.

Lines 12 uses the build key to look up the object in the current locator (as provided in the build context). By the way, this is why build keys should have value semantics: they're used as lookup keys for both policies and in the locator. If they compare by reference, later lookups will probably fail, as individual build key objects get recreated regularly.

If we find an object in the locator, we return it immediately (line 14). This short-circuits the rest of the strategy chain, which makes sense as the object is already created.

If the object is not found in the locator, then it needs to be created. Rather than do the work here, this strategy simply lets the strategy chain continue via a call to base.BuildUp (line 18).

Now that we can look stuff up in the locator, let’s look at the flip side, which is storing the created object in the locator. The implementation is equally straightforward:

1   class CacheStorageStrategy : BuilderStrategy {
2       public override object BuildUp(
3           IBuilderContext context,
4           object buildKey, 
5           object existing)
6       {
7           ICachingPolicy cachePolicy = 
8               context.Policies.Get<ICachingPolicy>(buildKey);
9           if(cachePolicy != null) {
10              if(cachePolicy.ShouldCache) {
11                  context.Locator.Add(buildKey, existing);
12              }
13          }
14          return base.BuildUp(context, buildKey, existing);
15      }
16  }

The overall skeleton of the code is identical to the CacheRetrievalStrategy – the caching policy is retrieved in the exact same way (lines 7-8). The big difference is on line 11. Here, instead of getting a value from the locator, we’re adding it. The object that’s being constructed (and therefore needs to be cached) is being passed in via the “existing” parameter. So we go ahead and put it in the locator if current policy settings say we should.

Finally, we call base.BuildUp again, so that if there are any strategies after this one they get a fair shot at the object.

We now have our strategies, so we need to add them to the builder. We can take advantage of the builder stages to make sure that the strategies are in the correct order. We’ll put the cache retrieval in the pre-creation stage, the creation strategy in the creation stage as before, and we’ll put the cache storage in the post-initialization stage. Our builder’s constructor looks like this:

1 	public CachingFactory()
2 	{
3 	  strategies.AddNew<CacheRetrievalStrategy>(BuilderStage.PreCreation);
4 	  strategies.AddNew<BasicCreationStrategy>(BuilderStage.Creation);
5 	  strategies.AddNew<CacheStorageStrategy>(BuilderStage.PostInitialization);
6 	  cache = new Locator();
7 	}

And with this, finally, that original test passes.

Where are we?

We’ve seen how to combine multiple strategies to implement more complex creation logic. We’ve also seen several of the options that ObjectBuilder provides for communication across strategies. These include:

  • Persistent policy objects so that the builder can configure how the strategies work.
  • A locator object to store objects across calls to BuildUp. Objects in the locator are typically indexed via build key (but can use any arbitrary object as long as it has compare-by-value semantics).
  • Passing the constructed object down the chain via the “existing” parameter so that later stages can work with or on the constructed object. We also make our first use of the build key to identity the objects we were creating, and look them up later.

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

Comments are closed