DX_SourceOutliner v2 Design Concepts

In a past post, I mentioned that I am in the process of declaring ‘Technical Bankruptcy’ on my DX_SourceOutliner project.  I wanted to share some of my thoughts on what’s wrong with the present approach to the design and what new ideas I have to try to address the problems.  As mentioned in my prior post, most of these issues are really about some decisions that I made in re: design of the application that weren’t really that limiting in the context of my own near-term goals for the tool but have turned into serious stumbling blocks when it comes to implementing some of the ideas that others have suggested to me since its initial release.

Problem 1: Procedural Code isn’t Extensible

Because the tool was originally limited in scope of what it would have to do with each node in the tree that it processed, it wasn’t at all unusual for there to be blocks of switch statements that looked something like this:

  1: switch case(element.ElementType) 
  2: { 
  3: 	case ElementType.Method: 
  4:  	{ 
  5: 		node = FormatMethodNode(element); 
  6: 		break; 
  7: 	} 
  8: 	case ElementType.Class: 
  9: 	{ 
 10: 		node = FormatClassNode(element); 
 11: 		break; 
 12: 	} 
 13: 	case ElementType.Variable: 
 14: 	{ 
 15: 		node = FormatVariableNode(element); 
 16: 		break; 
 17: 	} 
 18: } 

Then in each of the (conceptual) methods above, there would be code something like this…

   1: if (_showParamNamesInSignature) 



  2: {
  3: 	node.Text = string.Format("{0} ({1} {2})", element.Name, param.Type, param.Name);
  4: }
   5: if (_showToolTips) 



  6: {
  7: 	node.Tooltip = GetToolTipForElement(element);
  8: } 

This (overall) approach to processing each node made perfect sense when the scope of what needed to be done to each node, usually based on some combination of element type being processed and one or more options set in the UI (e.g., show names for params in signatures, etc.)  However, as the number of things that (might) need to be done to code element nodes starts to increase, we can see how this would lead to a steadily-increasing mess of spaghetti code where the number of places in the code that need to be touched just to add a new capability (e.g., provide different ways of handling nodes that are ‘filtered out’ based on pattern-matching of the element name) would start to grow unwieldy.

Since there wasn’t initially any intention on my part to extend the tool in any significant way, lack of an obvious extensibility point didn’t really matter much to me :)  But now that I find I want to try to accommodate some of the ideas for suggestions for alternate behavior of the tool, lack of a clear extensibility story in the code is a real hardship.

Problem 2: Decentralization of Decision Logic

The next problem the existing solution presents me with is the decentralization of decision logic that has spidered its way all over the place in my code.  Whenever I needed to interrogate a code element anywhere (e.g., in a node formatting method, etc.) the calling code just asks the ‘raw’ code element about itself.  Code (conceptually) like this is very common throughout the application…

   1: if (element as ElementWithParameters != null) 



   2:       //...do something with the parameters 



   3:

   4: if (element.Parent == LanguageElement.ElementType.Namespace)
   5:       //...do something with the element

This places the responsibility of deciding how and when to process a code element right inside the calling method, meaning that its scattered all over the place in the code. This makes changing any one of the rules (either in response to a changed user option or in response to a reported bug, etc.) quite difficult. Even just determining where in the code a particular formatting is being applied to one node vs. another is a more difficult task than it reasonably should be right now.

Build One to Throw Away

In this recent post I commented on the fact that in many other industries there is a common practice of prototyping.  In software development, this is often seen as a waste of resources, but in most other design-/problem-solving-oriented industries its not only standard accepted practice but a downright necessity.  Only after having built it one (usually sub-optimal but hopefully correctly functional) can one hope to learn enough to build it right.  This was certainly the case with the DX_SourceOutliner.

Domain Driven Design: Where’s the Domain in DX_SourceOutliner?

When I look back at the result of the v1.x effort, several common themes emerge from my retrospective that aren’t (presently) manifest as concepts in the software.  Whenever there are concepts in your solution that aren’t manifest in the code, there is unnecessary friction between your problem and your solution.  This is actually one of the tenets of Domain Driven Design the idea that the closer your software solution is to the domain of the problem you are trying to solve, the  more likely it is that behaviors, themes, delineations between components, and more in your solution will map 1:1 to the problem.

This increases the chances for experiencing that most wonderful of things: the ah-ha! moment when the alignment between problem and solution becomes so powerful that its immediately obvious where the next behavior in your code should belong and solutions to issues just flow naturally from the problem domain within which you are working.  As developers, hopefully we’ve all had the joy of working in such a system with so low a coefficient of friction between problem and solution that the two meld together into a single thing and the code begins to ‘just fit’ exactly where it seems ‘to belong’.

Observed Theme 1: Node Processing

Stepping back from the completed solution and looking at it again, one very clear theme that emerges from the solution is that of processing nodes in a tree.  Whether processed to set a tooltip value, processed to set an icon, or even (in the case of filtering) processed to actually remove it from the tree, processing nodes happens again and again in the solution.  But this concept, while pervasive throughout the solution, isn’t clear in the code.  The act of processing nodes is currently secreted behind a cacophony of arcane method names like…

  1: node.ToolTIp = GenerateToolTip(LanguageElement)
  2: node.Icon = SetIcon(LanguageElement)
  3: node = FormatMethodNode(LanguageElement) 

All of which are (sort of) explicit about what they are doing, but the calling code has to be intimately aware of how they work, what they do, how their return values are used to assign properties to the TreeNode instance, etc.  Its not bad, per se,  but with the introduction of a common concept (in code) behind them, it could be better.

How do we represent a common concept in this code?  With an Interface.  In this case, we need an interface that takes a node, does something with it, and returns the processed node back to the caller.  Something like this…

  1: public interface INodeProcessor
  2: {
  3: 	TreeNode ProcessNode(TreeNode node);
  4: }

Implementers of this interface are agreeing that they take a node, process it (in some arbitrary, who-cares, kind of way) and return the node back to the caller.  A (dirt-simple) example of this might be…

  1: public class IconAssignmentProcessor :INodeProcessor
  2: {
  3: 	private string GetIconName(LanguageElement element)
  4: 	{
   5: 				if (element.ElementType == ElementType.Method)
   6: 				return “method.png”;
  7: 	}
   8:

  9: 	public TreeNode ProcessNode(TreeNode node)
 10: 	{
 11: 		node.Icon = GetIconName((LanguageElement)node.Tag.Element);
 12: 				return node;
 13: 	}
 14: }

This approach neatly encapsulates the details of how and why an icon gets assigned to a treenode in such a way that calling code simply passes it an existing node, gets the same node back, and moves on.  The messy details of what might happen to the node before its returned (and why) aren’t any concern of the calling code.

Using this approach,we can imagine other implementers of this interface like ToolTipProcessor (knows how to compose a tooltip for each code element type and set it on the node), DisplayTextProcessor (knows how to compose the text name of the node and include return types, argument names, etc.), TypeVisibilityProcessor (knows how to toggle visibility for nodes based on user settings to include/exclude methods, variables, etc.), NameFIlterProcessor (knows how to toggle visibility based on pattern-matching to the user-supplied name filter value), and more.

We can even imagine that if/when other ideas about things that need to be ‘done’ to nodes emerge (like SyntaxColoringProcessor that might manage to use Visual Studio code syntax coloring settings to color-code the text of treenodes as someone had suggested would be helpful to support), this design provides an obvious (and simple to implement) extension point for such things.

Observed Theme 2: Tree Processing

Another theme that emerges from looking back at the existing implementation is that of processing (or perhaps ‘transforming’) the complete tree structure itself.  Rather the ‘node processing’ that is about making changes to one node at a time in complete isolation, ‘tree processing’ is instead about really making transformative changes to the entire tree structure as a whole.

Again, common concepts in code are (often) represented in an Interface and we can design one that takes a tree, does something with it, and returns the tree back as so…

  1: public interface ITreeProcessor
  2: {
  3: 	TreeView ProcessTree(TreeView tree);
  4: }

Very similar in concept even if very different in implementation from the INodeProcessor interface, this interface contract also takes an input, does something arbitrary to it, and returns it back to the caller.  An (overly simplistic) implementation of this interface might be something like the following example…

  1: public class InvisibleNodePruner : ITreeProcessoressor
  2: {
  3: 	TreeView ProcessTree(TreeView tree)
  4: 	{
  5: 		foreach (TreeNode node in tree.nodes)
  6: 		{
   7: 						if (!node.IsVisible)
  8: 			{
  9: 				tree.Nodes.Remove(node);
 10: 			}
 11: 		}
 12: 								return tree;
 13: 	}
 14: }

In this example. we are iterating through all the nodes in the tree, checking to see if something else (e.g., some prior INodeProcessor implementation that’s been applied to all nodes of the tree) has set the .IsVisible property to false.  If its false, we simply remove the node from the tree and move on to the next node, processing them all until we’re done (at which point we simply return the tree to the caller.

There are (obviously) all kinds of complexities in this approach that this algorithm fails to address (what about recursing through all the children of all of the tree’s top-level nodes?, what if a top-level node that’s not visible contains children nodes that are visible?, etc.), but one can already see that this design approach will neatly encapsulate all of that messy ‘goo’ inside each of the ITreeProcessor0implementing classes, right where it belongs (and out of sight, out of mind too). 

Other implementers of the ITreeProcessor interface might include classes like TreeAlphabetizer (reordering the nodes of the tree in alphabetical rather than hierarchical order), etc.

And just as in the case of the INodeProcessor interface, the ITreeProcessor interface makes it very obvious to see where the future extensibility points are for the behavior of the DX_SourceOutliner.  If (as several have suggested) they want to see the tree (somehow!) filtered and sorted but retain its hierarchical structure rather than collapsing to a ‘flattened’ tree or list-view-style, then this admittedly messy and complex tree-restructuring code can easily be placed into an AlphabetizedAndHierarchicalTreeProcessor class when the time comes to implement this capability.

More to Come

There are several other nascent themes observed in the existing solution as well; more on those in a subsequent post, but this (above) is a general idea of how I am taking the opportunity to re-think the problem.