DX_SourceOutliner v2.0 Redesign Concepts (final installment)

This is the last in a series of posts here and here about my design ideas for re-engineering the DX_SourceOutliner to improve its design as well as increase its flexibility and extensibility for the future.

Observed Theme 4: Analysis of Code Element Metadata

The .NET WinForms TreeNode class provides a node.Tag property of type object, the intended use of which is for the developer to ‘attach’ arbitrary data to each node.  In the case of the DX_SourcedOutliner, I’m leveraging this property of each node to store the actual DXCore ‘code element’ that is represented by each tree node.  In the initial construction process of the treeview itself, there is code along the lines of the following that accomplishes this…

public void BuildTree()
{
	foreach (LanguageElement element in ActiveTextDocument.Nodes)
	{
		TreeNode node= new TreeNode();
		node.Tag = element;
		//do more setting of props for the treenode
	}
}

At repeated points in the existing application, the code needs to inspect the ‘code element’ related to the node in the tree in order to determine how to process it.  And this approach makes it simple for calling code to ‘get at’ the underlying DXCore LanguageElement when needed to determine how to process a node.  For example, when it comes time to assign an icon to the treenode, the code can easily do things like this to extract tyhe underlying code element from the node’s .Tag property to inspect it and make decisions about the LanguageElement metadata it is able to extract from the code element instance….

public void AssignIcons()
{
	foreach (TreeNode node in nodes)
	{
		LanguageElement element = node.Tag as LanguageELement;
		if (element.LangaugeElementType == LanguageElementType.Method)
		{
			node.IconKey = "Method.png";
		}
		//handle other types here...
}

But the problem with this approach is that the calling code has to be completely able to understand the structure and properties of the underlying LanguageElement in order to ‘inspect’ it properly to ensure it takes appropriate action.  And some interrogation of the underlying LanguageElement isn’t as entirely simple as the above example would suggest.  In many cases it’s necessary to perform more complex processing of the LanguageElement in order to ‘analyze’ it to guide the construction and formatting of the tree.  What’s needed is something that can support accreting all of the analysis processing of LanguageElements into a single class that can centralize the metadata extraction process.

Enter the SourceTreeNode

It became clear to me that what was needed was a special kind of TreeNode that could not only perform the role of the WinForms TreeNode class, but also ‘carry’ extra ‘metadata’ about the source code element related to the TreeNode.  In a perfect world, I would be able to simply derive from the framework’s own TreeNode class and extend the properties and methods as needed along the lines of something like this…

public class SourceTreeNode : TreeNode
{
	public LangaugeElement Element { set; private get; }
	public LanguageElemement.ElementType ElementType { private set; get; }
	public bool IsVisible { private set; get; }
	public IDictionary<string, string> Parameters { private set; get; }
	//more specific properties about the code element here
}

Its not clear to me yet whether its reasonable to subclass the frameworks’ own TreeNode class as shown above without breaking a universe of related things in the framework.  In my past experiences with subclassing WinForms controls (admittedly from some time ago under .NET 2.0 since I’m not really a professional WinForms application builder any more) most of the challenges have come from introducing breaking changes to the controls’ eventing model but its possible that this can still be achieved with the TreeNode control without undue effort needed.

But even if this proves challenging, I will simply create my own SourceTreeNode class without subclassing TreeNode and jam the SourceTreeNode instance into each TreeNode’s own .Tag property.  In this way, I can still centralize the ‘code-element-analysis’ logic into the single SourceTreeNode class rather than leave it scattered all over the codebase.  SourceTreeNode will be responsible for taking a LanguageElement instance in its constructor and setting all its properties as needed to reflect the results of its analysis of the provided language element.  I’m imagining something along the lines of this approach…

public class SourceTreeNode
{
	public SourceTreeNode(LanguageElement element)
	{
		ElementName = element.Name;
		ElementType = element.LanguageType.ToString();
		Accessibility = element.MemberVisibility.ToString();
		//perform remaining analysis of LanguageElement here
	}
}

Then code that needs to inspect ‘properties’ of the code element related to the tree node being processed, formatted, etc., need only interrogate simple properties of the SourceTreeNode object in order to get values to display, guide formatting, etc.  A further advantage of this approach is that this introduces a disintermediation layer between the CodeRush LanguageElement and the DX_SourceOutliner code that needs to interrogate properties of the code element to format the tree.

Observed Theme 5: Iteration

The fifth and final theme that I have observed in the existing v1.x codebase is that of iteration.  As you can imagine, there’s a lot of places that iteration needs to happen in the DX_SourceOutliner.  Whether processing nodes to add tooltips, processing nodes to set icons, or processing nodes in the code DOM to add them to the tree in the first place, there’s an awful lot of iterating through nodes going on.

And since all of these nodes are somewhere in one or more hierarchical tree structures, all of this iterating has to (usually) happen recursively from ultimate top ancestor to each of parent siblings to each parent’s children to each of their grandchildren, to each of their great-grandchildren, and so on and so on from the root of the tree to the final outermost leaves.  As software developers, we can all (I hope!) write a tree-traversing recursive algorithm in our sleep without much concentration, but it still doesn’t mean that its a good idea to do so over and over again everywhere its needed in code!  The existing solution is full of code like this…

public void SetIcons(IEnumerable<TreeNode> nodes)
{
	foreach (TreeNode node in nodes)
	{
		SetIcon(node);
		foreach (TreeNode childnode in node.Nodes)
		{
			SetIcon(childNode);
			SetIcons(childNode.Nodes);
		}
	}
}

public void SetIcon(TreeNode node)
{
	//do something to determine the right icon to set...
	node.IconKey = "Public_Method";
}

There’s nothing either complicated or error-prone about this kind of code – as mentioned, we’ve probably all written code like this 1000s of times in our careers to process recursive hierarchies of tree-structured data.  But its implementation over and over again demonstrates an important pattern in the DX_SourceOutliner solution that deserves a position of primacy within the code.

Enter: The TreeWalker Role

To handle this responsibility of traversing a hierarchical tree data structure, the next version of the DX_SourceOutliner will introduce the notion of the TreeWalker object.  The TreeWalker will know nothing but how to traverse (walk) a tree and apply one or more changes to each node it finds within the tree.  Its expected to have an interface somewhat like the following…

public Interface ITreeWalker
{
	void ProcessTree(IEnumerable<TreeNode> nodes, IEnumerable<INodeProcessor> processors;
}

A very simple interface (as really all of the suggested classes for the v2 engineering effort have become), the ITreeWalker interface is a contract that says “give me a list of nodes and a list of processors for those nodes and I will apply each of the processors to each of the nodes I find anywhere in the tree”.  Now, rather than the tree-iteration code being scattered all over the solution in each of the classes that needs to traverse the tree and set properties on nodes, etc., its all centralized and written exactly once so that it can be used to recursively apply one or more INodeProcessors to the nodes of the tree.

Putting it all together: Funny How All Good Code Ends Up Being S.O.L.I.D.

Readers of my blog will know that I am a strong proponent of the so-called S.O.L.I.D. OO design principles originally documented and codified by Robert C. (Uncle Bob) Martin.  Its my experience that OO software designs ignore these principles at their own peril and that paying attention to these principles almost always leads to a better, more coherent, extensible, and maintainable solution.  Its interesting to me that the reverse actually holds true as well: if you analyze a solution that’s got maintainability and extensibility as an underlying design value, one can often uncover many of the S.O.L.I.D. principles lurking within the design, just beneath the covers.  SOLID leads to good design and good design encapsulates SOLID principles.  Let’s see about that…

So now we have a coherent object model with clear roles and responsibilities within the system.  Each class is responsible for just one thing (the Single-Responsibility Principle), each class has a clear extensibility point for its role in the system (the Open/Close Principle), each class is delegating the HOW of what gets done to a subcomponent of the system (the Dependency Inversion Principle) even as it retains control over WHAT gets done.

The TreeOptions class encapsulates the user-selected configurations that will affect the composition of the tree.  The TreeProcessorSelector class will consume these options to decide which of one or more NodeProcessors to choose to apply to the tree.  The SourceTreeNode class will encapsulate the code-element meta-data that the NodeProcessors will need to determine how to set values for the TreeNodes.  The TreeWalker will traverse the nodes in the tree, applying the list of TreeNodeProcessors to each of the nodes found.

At relevant steps along the way, various parts of the program can make the choice to substitute different implementations of what are ultimately a set of very simple interface contracts in order to completely change the behavior of the system without changing the underlying structure of the application in re: where its logic for its various tasks is compartmentalized and isolated.

TDD Possible in a Visual Studio Add-in…?

As mentioned before, another side-effect of this more separated design approach is that it should start to become possible to unit test more of the behavior of the system OUTSIDE of the Visual Studio runtime and disconnected from the UI layer, making it (theoretically) possible to develop some or all of these classes in a more ‘traditional’ TDD (test-first) manner that the initial approach and design just simply didn’t support.  Launching a second instance of VS to host your tests within just isn’t compatible with the idea of run-the-whole-suite-of-tests-to-get-rapid-feedback that effective TDD really requires of your development environment.

We’ll see how that proposal holds up as I get into the actual coding of this thing over the next few weeks…

More to come~!