DSL Tools: A Complex Solution for a Simple Problem
Last night I was unable to sleep, so I started reading Domain Specific Development with Visual Studio DSL Tools
and I was immediately reminded of my rant about software factories. Just like with the software factories provided by the Patterns and Practices team, the examples in the book are bad examples of what you should use a visual DSL for. The book has an introductory narrative which guides us through how the fictional consultancy CJKW creates a DSL to customize the ASP.NET Issue Tracker starter kit. CJKW's motivation for doing this is that their various clients have different requirements for which states that apply to an issue and the manual modifications for the different installations have become increasingly difficult to maintain.
The lead role in the story is played by Devika, a lead programmer at CJKW. The narrative leads us through the entire process from creating a DSL project, defining the DSL definition with all its elements and constraints, customizing the design time experience, creating templates for the modifications that have been manual to packaging and deploying the DSL on developer computers. All in all this is a lengthy and complex undertaking. The irony of the story is that after Devika has spent a fair share of time developing the DSL, CJKW's business analyst objects to the design because most clients want to change the states and transitions midway through a project. Regenerating the entire application from the DSL just won't be acceptable to their clients. Luckily, Archie (CJKW's big-A Architect) steps in at the last moment and suggest that the issue tracking application should be rewritten to be more generic, storing the allowed states and transitions in the issues database itself. Devika's DSL can be tailored to populate these tables and the day is saved. Everybody is happy, but should they be?
The story shows us how to create an utterly complicated solution for a simple problem. There is no example of how CJKW retrieve the states and transitions from a database so let's take a look at the code generated by Devika's DSL (which is similar to the manual hacks CJKW had done to satisfy their clients before they discovered DSLs).
string[] nextOptions=new string[] {};
if (!string.IsNullOrEmpty(currentState))
{
if ("Unassigned"==currentState)
{
nextOptions=new string[]
{
"Unassigned", // allow leaving state unchanged
"Assigned",
"Rejected"
};
}
if ("Approved"==currentState)
{
nextOptions=new string[]
{
"Approved", // allow leaving state unchanged
"Fixed"
};
}
if ("Rejected"==currentState)
{
nextOptions=new string[]
{
"Rejected" // allow leaving state unchanged
};
}
if ("Fixed"==currentState)
{
nextOptions=new string[]
{
"Fixed" // allow leaving state unchanged
};
}
}
else
{
// New issue
nextOptions=new string[] {"Unassigned"};
}
A couple of things to notice here is that the code is procedural and repetitive. The reason for this is that the code is generated from a template and it is much easier to generate a single procedure than an object model. Astute readers might also have noticed the bug in the procedure. When the state changes from "Unassigned" to "Assigned" we reach a dead end, there are no subsequent states following the "Assigned" state. These sorts of bugs can easily crawl into code that suffers from primitive obsession.
In my rant on software factories I stated that we should prefer frameworks over code generation, and whenever we see a reoccurring problem we should try to commoditize it via a framework rather than through code generation to improve maintainability.The same goes for generative DSLs. Let's refactor!
We'll start by creating a kind of class I'm not particularly fond of, the helper class, but its the best option here since the code is primitive obsessed.
public class StateTransition
{
private string initialState;
private Dictionary<string, string> transitions;
public StateTransition(string initialState, Dictionary<string, string> transitions)
{
this.initialState = initialState;
this.transitions = transitions;
}
public string[] GetSuccessiveStates(string currentState)
{
if (string.IsNullOrEmpty(currentState))
{
return new string[] { initialState };
}
if (!transitions.ContainsKey(currentState))
{
return new string[0];
}
return transitions[currentState].Split(',');
}
}
Rather than newing up instance of the helper class, we'll use inversion of control to create the new instances. Below is the Castle Windsor configuration for the component.
<component
id="StateTransition"
type="Example.StateTransition, StateTransitionExample">
<parameters>
<initialState>Unassigned</initialState>
<transitions>
<dictionary keyType="System.String, mscorlib" valueType="System.String, mscorlib">
<entry key="Unassigned">Unassigned,Assigned,Rejected</entry>
<!-- I wonder what happens if we go from Unassigned to Assigned :P -->
<entry key="Approved">Approved,Fixed</entry>
<entry key="Rejected">Rejected</entry>
<entry key="Fixed">Fixed</entry>
</dictionary>
</transitions>
</parameters>
</component>
Rather than going down the enterprisey route of storing the states and transitions in the database we're keeping them in the configuration file.
Next, we get rid of all the code generated by the DSL and replace it with this:
StateTransition stateTransition = IoC.Resolve<StateTransition>();
string[] nextOptions = stateTransition.GetSuccessiveStates(currentState);
We have now satisfied both of the requirements that lead to the creation of the DSL; maintainability (less code, no string literals in code and more) and configurability (states and transitions pulled out from the application). The cool thing is that we've done this in a fraction of the time it would have taken to create the DSL and make the required changes to the Issue Tracking application.
The state transition bug (from "Unassigned" to "Assigned") has intentionally been left in. Let's look at how we use the State pattern to prevent glitches like this from creeping into our code.
Above you can see a class diagram of the design. This pattern allows a class to change its behavior when its state changes, and it is a brilliant example of why you should prefer composition over inheritance.
When a client creates an instance of the Issue class, its state field is set to an instance of the UnassignedState class. Whenever the client class a method on the Issue, these invocations are delegated to the IssueState instance assigned to the state field. Depending on which state we are in, the behavior of the Issue class changes. Below is an example of a concrete IssueState:
public class UnassignedState : IssueState
{
public UnassignedState(Issue issue) : base(issue)
{
}
public override void Assign()
{
issue.State = new AssignedState(issue);
}
public override void Reject()
{
issue.State = new RejectedState(issue);
}
}
I reckon that changing the state leads to other things than just updating a database column, so something along the lines of this design would probably have been more appropriate for the Issue Tracker application than the procedural design it has. This is one of the big issues with the examples provided, they encourage hard to maintain procedural code because this is easy to generate.
My post on software factories got some feedback from Microsoft on how great software factories are. There is nothing wrong with the guidance automation or DSL Tools themselves, but I would really like to see some guidance on how to use them that show how they add value rather than how to create second rate solutions for problems that have much simpler solutions. If you want to see a good application of the DSL Tools I recommend Gokhan Altinoren's ActiveWriter which is a visual DSL for generating ActiveRecord classes.