|
view as multiple pages
BY: Max Guernsey, III - Managing Member, Hexagon Software LLC
Introduction
Circular references are structures in which, by following some sequence of relationships longer than one, an object can refer to itself. The references do not have to be visible to the outside world, they just have to exist.
An example of a circular reference would be a traditional marriage: the husband has a relationship to his wife who, in turn, has a relationship back to her husband. Therefore, assuming no access control, the assertion husband.Wife.Husband == husband should, or at least can, be true.
These kinds of structures represent a unique construction challenge when they must be solidified as part of instantiation, as is often the case. An example of when a circular reference must be defined in construction would be a complex strategy object that must be able to recur. In such a case, the leaf strategy instances need a reference to the object that represents the logical strategy to which they are a party. The next section is an incomplete example of this kind of structure.
Example: Circular Reference (incomplete)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
public interface Strategy
{
void Process(object argument);
}
internal class UnwrapList : Strategy
{
private readonly Strategy _Whole;
public void Process(object argument)
{
IList argumentAsList;
try
{
argumentAsList = (IList)argument;
}
catch (InvalidCastException)
{
return;
}
foreach (object o in argumentAsList)
{
_Whole.Process(o);
}
}
}
Neat... but how?
In the previous example, _Whole can and should be immutable (readonly or final); a partial strategy should only be part of one whole. When a field is immutable, it must be initialized in its object’s constructor. This introduces a “chicken or egg” problem. If you build the containing strategy first, how do you tell it about this content strategy? Likewise, if you build the content strategy first, how can you give it a reference to the yet un-built owner strategy?
The answer is so simple that I smacked myself on the head when it finally struck me. You have to build the objects at the same time: Those which are codependent must be instantiated concurrently. Incidentally, that rule is probably as close to “beautiful” as anything that isn’t also fleeting will ever get.
Now, obviously, when I say “at the same time,” I can’t really mean at the same instant
– that would take threading, multiple cores, and a lot more work than is really warranted.
So what does that really mean? It means that the two constructors
must, at some point, be running simultaneously; that both have started and neither have finished.
In implementation terms that translates into one constructor delegating to another.
So how do we achieve this? Let’s look at a narrow case, first: the situation in which there is a
clear parent/child relationship, the parent knows the type of its children, and the children know
that they are, in fact, children. This imposes a sort of tree-like build behavior on the entire graph. You plant the seed (invoking the constructor of the parent class), then the seed sprouts into the trunk (the initializing parent object), the trunk sprouts branches (delegating to the constructors of child objects), and so on. The next section contains an example.
Example: Circular References (closed to addition)
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
public interface Strategy
{
void Process(object argument);
}
internal class CheeseFinder : Strategy
{
private readonly Strategy _UnwrapList;
private CheeseFinder()
{
_UnwrapList = UnwrapList.GetInstance(this);
}
public Strategy GetInstance()
{
return new CheeseFinder();
}
public void Process(object argument)
{
if (Equals(argument, "cheese"))
{
Console.WriteLine("Found cheese!");
}
_UnwrapList.Process(argument);
}
}
internal class UnwrapList : Strategy
{
private readonly Strategy _Whole;
private UnwrapList(Strategy root)
{
_Whole = root;
}
public static Strategy GetInstance(Strategy root)
{
return new UnwrapList(root);
}
public void Process(object argument)
{
IList argumentAsList;
try
{
argumentAsList = (IList)argument;
}
catch (InvalidCastException)
{
return;
}
foreach (object o in argumentAsList)
{
_Whole.Process(o);
}
}
}
Inverting Responsibility for Construction of Child Nodes
In that last example, CheeseFinder hunts for the string “cheese.” It knows how to navigate through anything that implements IList by way of its child strategy UnwrapList. During the constructor, it delegates to UnwrapList’s “GetInstance” method, thereby causing the two constructors to be executing at the same time. The CheeseFinder object passed a reference to itself into the UnwrapList instance’s GetInstance method, so it knows how to re-delegate to the CheeseFinder.
Unfurl the banner and don your flight-suit: Mission complete! We have constructed an unbreakable codependency (in this case: a good thing). UnwrapList is a reusable class that can be a party to any complex implementation of the Strategy interface. Whenever we want to create a new complex strategy implementation (“WineFinder,” perhaps?), we just delegate to UnwrapList.GetInstance(Strategy) and pass in a reference to the constructing parent object.
So what if we want to be able to swap out the UnwrapList strategy for some other implementation?
The individual class designs certainly permit this. However, the coupling from the parent types (WineFinder, CheeseFinder, etc.) to the child types (such as UnwrapList) prevents us from swapping out one kind of child strategy for another. Let’s consider an alternative implementation that addresses this need. (In the next section)
Example: Open to Addition
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
public interface Strategy
{
void Process(object argument);
}
internal interface ChildStrategyFactory
{
Strategy GetChildStrategy(Strategy parentStratagy);
}
internal class CheeseFinder : Strategy
{
private readonly Strategy _Unwrap;
private CheeseFinder(ChildStrategyFactory unwrapFactory)
{
_Unwrap = unwrapFactory.GetChildStrategy(this);
}
public Strategy GetInstance(ChildStrategyFactory unwrapFactory)
{
return new CheeseFinder(unwrapFactory);
}
public void Process(object argument)
{
if (Equals(argument, "cheese"))
{
Console.WriteLine("Found cheese!");
}
_Unwrap.Process(argument);
}
}
internal class UnwrapList : Strategy
{
private class Factory : ChildStrategyFactory
{
public Strategy GetChildStrategy(Strategy parentStratagy)
{
return new UnwrapList(parentStratagy);
}
}
private readonly Strategy _Whole;
private UnwrapList(Strategy root)
{
_Whole = root;
}
public static ChildStrategyFactory GetFactory()
{
return new Factory();
}
public void Process(object argument)
{
IList argumentAsList;
try
{
argumentAsList = (IList)argument;
}
catch (InvalidCastException)
{
return;
}
foreach (object o in argumentAsList)
{
_Whole.Process(o);
}
}
}
We've Broken Through!
Now we're getting somewhere: the implementations of _Unwrap and _Whole can vary
independently of each other while depending upon one another to do their respective jobs.
This, after all, is the point of a Strategy: to allow a client to depend upon a service for
certain behavior without coupling to exactly what that service does. In this case, we’ve
gone one step further. The term “client” and “service” no longer really apply, except
maybe when evaluated in a very narrow context. Instead you just have entities in a system
exchanging information, dispatching requests to related entities, and fulfilling requests
to the best of their ability. In essence, we’ve promoted our system from having a
client/server relationship to being a microcosmic economy.
That’s a nice mark to have met. It frees us, ultimately, from a lot of
unnecessary design. Instead of fighting an unnatural structure, we can focus on
delivering value – which is the fun part, right? We’re not quite done, yet. The
previous statements actually belie questions that arise from our little discovery
that a graph of objects can be an economy (as opposed to a hierarchy of lords and
vassals).
If we think about it for a while, we probably all come up with the
same question: is there a way to abandon the “parent/child” part of this structure?
The answer is simple but the road to get there is actually kind of arduous. I’ll spare
you the painful lessons I learned and cut to the chase at this point. If you’re
interested in those steps, post a question in
our chat room and we’ll
strike up a conversation. If you’re sold and just want to see how we address the problem,
then read on...
What We Did
We addressed the problem in a reusable way. We put the solution into Common Components,
which any of you are free to download and use – there is no license fee.
Let’s review all of our goals because some of them were left implied early on.
We have the following requirements:
1. We need to be able to build graphs of objects that contain circular references set during construction.
2. Our system should not care what the structure of the resulting object structure is.
3. You should be able to build as many graphs as you like from the same system of builder objects.
4. If the entities responsible for building graph-nodes are compliant, you should be able to build two graphs simultaneously on separate threads using the same system of objects.
Now that Common Components meets the above requirements, we can use it to solve the problem we've identified
with our example code as in the following example. (Example in the next section)
Example: Using Common Components
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using HexagonSoftware.Common.Contracts;
public interface Strategy
{
void Process(object argument);
}
internal abstract class StrategyBase : Strategy
{
protected StrategyBase(GraphNodeStagingPoint<Strategy> stagingPoint)
{
stagingPoint.SetGraphNode(this);
}
public abstract void Process(object argument);
}
internal class CheeseFinder : StrategyBase
{
public sealed class Builder : GraphNodeBuilder<Strategy>
{
private GraphNodeBuilder<Strategy> _Unwrap;
private Builder() { }
public GraphNodeBuilder<Strategy> Unwrap
{
get { return _Unwrap; }
set { _Unwrap = value; }
}
public void Build(GraphNodeStagingPoint<Strategy> stagingPoint)
{
new CheeseFinder(stagingPoint, this);
}
public static Builder GetInstance()
{
return new Builder();
}
}
private readonly Strategy _Unwrap;
private CheeseFinder(
GraphNodeStagingPoint<Strategy> stagingPoint,
Builder builder)
: base(stagingPoint)
{
_Unwrap = stagingPoint.StagingArea.GetNode(builder.Unwrap);
}
public override void Process(object argument)
{
if (Equals(argument, "cheese"))
{
Console.WriteLine("Found cheese!");
}
_Unwrap.Process(argument);
}
}
internal class UnwrapList : StrategyBase
{
public class Builder : GraphNodeBuilder<Strategy>
{
private GraphNodeBuilder<Strategy> _Whole;
private Builder() { }
public void Build(GraphNodeStagingPoint<Strategy> stagingPoint)
{
new UnwrapList(stagingPoint, this);
}
public GraphNodeBuilder<Strategy> Whole
{
get { return _Whole; }
set { _Whole = value; }
}
public static Builder GetInstance()
{
return new Builder();
}
}
private readonly Strategy _Whole;
private UnwrapList(
GraphNodeStagingPoint<Strategy> stagingPoint,
Builder builder) : base(stagingPoint)
{
_Whole = stagingPoint.StagingArea.GetNode(builder.Whole);
}
public override void Process(object argument)
{
IList argumentAsList;
try
{
argumentAsList = (IList)argument;
}
catch (InvalidCastException)
{
return;
}
foreach (object o in argumentAsList)
{
_Whole.Process(o);
}
}
}
public static class DemonstrateUse
{
public static void Go()
{
// 1. Get builders
CheeseFinder.Builder cheeseFinderBuilder =
CheeseFinder.Builder.GetInstance();
UnwrapList.Builder unwrapBuilder =
UnwrapList.Builder.GetInstance();
// 2. Set references of builders
cheeseFinderBuilder.Unwrap = unwrapBuilder;
unwrapBuilder.Whole = cheeseFinderBuilder;
// 3. Get a staging area
Factory factory = FactoryLocator.GetFactory();
GraphStagingArea stagingArea =
factory.CreateGraphStagingArea();
// 4. Build graph by getting reference to a node in it
Strategy cheeseFinder =
stagingArea.GetNode(cheeseFinderBuilder);
}
}
Conclusion
First: note that the Builder classes do not assign anything or return anything. They
simply instantiate the object in question and respect its ability to properly
register itself with the staging point. Secondly, note that all graph nodes
(implementations of Strategy, in this case) do, in fact, register themselves with their
staging points first thing (by way of their base constructor).
The class named “DemonstrateUse,” demonstrates how this system is used. There are four
steps: getting the builders, linking the builders, getting a staging area, and putting
it all together.
You may question “how is this any better than just having a method
on Strategy that lets you set a reference to the object representing the whole?”
There are a number of reasons. The most prominent reason is that it breaks object
creation into multiple phases so that you never have partially initialized objects.
It also impacts the quality of your design by centralizing the relational concerns
pertinent to an object in its Builder.
The most visible reason why this is better than having mutable strategies is that it lets
you create complex object structures in manageable stages. First you set up a network
of builders. Then you produce a graph of “usable” objects. At no point does an actual
Strategy object exist that is “partially initialized.” The builders are never really
“not initialized” even though they do start out “not configured.” On the other hand,
if you had a method on the Strategy interface that accepted a reference to the “whole”
strategy, you would start out with partially initialized strategies and then work your
way towards complete ones.
A subtler reason, with possibly greater consequences, is the one mentioned in an earlier section:
we want to encapsulate the dependencies of one strategy from any entity that does not need to know about them. If you have a “SetReferenceToWhole” method, then who calls that method? Who decides what the “whole” is? How do you handle the need for certain strategies to delegate along a different axis?
So the short answer is that just having a multi-stage initialization process with a “SetReferenceToWhole” method is too confining in our design and too permissive of misuse – exactly the opposite of what we want.
-- Max Guernsey, III / Managing Member / Hexagon Software LLC
Related Links:
Common Components
|