Introduction
I was recently immersed in a great book, "Agile Principles, Patterns, and Practices in C#" by Micah Martin and Robert C. Martin (Pearson Education), and got intrigued by the authors State Machine Compiler, written in Java, which can generate Java, C++ and C# code, given a standardized input format. Since the meds for my Java allergy did not fully kick in, I started to wonder if I could replicate the SMC with a T4 template.
Background
"Finite state automata are among the most useful abstractions in the software arsenal and are almost universally applicable. They provide a simple and elegant way to explore and define the behavior of a complex system. They also provide a powerful implementation strategy that is easy to understand and easy to modify." Martin, Micah; Martin, Robert C. (2006-07-20). Agile Principles, Patterns, and Practices in C# (Kindle Locations 10069-10071). Pearson Education. Kindle Edition.
There are many ways to implement a Finite State Machine. I truly like the elegance and style as demonstrated in the book. However, it still required me to have a set of predefined classes outside of the state machine itself. While this might be acceptable in most cases, it was not applicable to my project, where I often require dynamic compilation and load of classes. As such, I have slightly modified the book's sample, to allow for all related classes to be stored in one namespace. This might result in partial code duplication, but in my case it was worth the risk.
Using the code
To be able to use object initializers and LINQ queries inside a T4 template, I have decided to create a few helper classes first:
namespace FiniteStateMachine
{
public class FSMMachine
{
public string Name { get; set; }
public string InitialState { get; set; }
public FSMState[] States { get; set; }
}
public class FSMState
{
public string Name { get; set; }
public FSMEvent[] Events { get; set; }
}
public class FSMEvent
{
public string Name { get; set; }
public string NewState { get; set; }
public string Action { get; set; }
}
}
It is quite simple really:
FSMMachine
contains basic definitions about my state machine, as in Name
and InitialState
, plus an array of allowed States
FSMState
has a Name
property, and a set of Events
it recognizesFSMEvent
has a Name
, NewState
and Action
properties
To define a state machine in my T4 template, I can simply use:
const string sLocked= "Locked";
const string sUnlocked= "Unlocked";
const string eCoin= "Coin";
const string ePass="Pass";
const string aUnlock="Unlock";
const string aAlarm="Alarm";
const string aLock="Lock";
const string aThankYou="ThankYou";
var stateMachine = new FSMMachine{ Name = "Turnstile", InitialState = sLocked, States = new FSMState[] { new FSMState
{
Name = sLocked,
Events = new FSMEvent[] { new FSMEvent
{
Name = eCoin,
NewState = sUnlocked,
Action = aUnlock
},
new FSMEvent
{
Name = ePass,
NewState = sLocked,
Action = aAlarm
},
}
},
new FSMState
{
Name = sUnlocked,
Events = new FSMEvent[] { new FSMEvent
{
Name = eCoin,
NewState = sUnlocked,
Action = aThankYou
},
new FSMEvent
{
Name = ePass,
NewState = sLocked,
Action = aLock
},
}
}
}};
var events = stateMachine.States.SelectMany(s => s.Events.Select(s2 => s2.Name)).Distinct();
var actions = stateMachine.States.SelectMany(s => s.Events.Select(s2 => s2.Action)).Distinct();
This roughly resembles the format originally published in the book.
FSMName Turnstile
Context TurnstileActions
Initial Locked
Exception FSMError
{
Locked
{
Coin Unlocked Unlock
Pass Locked Alarm
}
Unlocked
{
Coin Unlocked Thankyou
Pass Locked Lock
}
}
Since my context is isolated to the generated namespace, I did not have a need to create a special property for it in my FSMMachine
class. Similarly, each generated namespace has its own FSMError
implementation.
You can also notice that all my states, event and actions are defined as string constants. I find it more convenient to use constants to avoid typing mistakes and streamline the machine definition.
When you save a T4 template, the built-in Custom Tool starts generating the output as defined by the T4 statements.
For example, this T4 template:
<#@ template hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>
Hello, world!
would result in creating a text file, with the same name as the name of the template, and a single line saying hello to the entire world. There are many constructs you can leverage in a T4 template, including LINQ and any of your custom libraries. To import an assembly, you must use:
<#@ assembly name="System.Core" #>
<#@ assembly name="..\FSM.dll" #>
This will reference the System Core library, as well as the custom helpers classes we have initially created.
Second, just like in C#, you must import the namespace you need to process the custom template:
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="FiniteStateMachine" #>
<#@ output extension=".cs" #>
Now you are ready to generate an IController
interface using all possible actions, with the template looking like this:
public interface I<#= stateMachine.Name #>Controller
{
<# foreach(string act in actions){#>
void <#= act #>();
<# } #>
}
which given the above state machine definition will generate an interface like so:
public interface ITurnstileController
{
void Unlock();
void Alarm();
void ThankYou();
void Lock();
}
This can be repeated over and over, with full support of the C# language, generating lines upon lines of C# code, which would otherwise have to be keyed in by hand.
I have attached the FSM helper class along with the FSM T4 template and a sample generated state machine code, using the sample definition.
This technique turned out to be a great time saver for me. I hope it will help you, too. Here is a test case, a slightly modified copy from the book, to confirm that the generated state machine code behaves as designed.
namespace FSMTests
{
using NUnit.Framework;
using TurnstileMachine;
[TestFixture]
public class TTTurnstileTests
{
#region Fields
private TurnstileControllerSpoof controllerSpoof;
private TurnstileMachine turnstile;
#endregion
#region Public Methods and Operators
[Test]
public void CoinInLockedState()
{
this.turnstile.SetState(new Locked());
this.turnstile.Coin();
Assert.IsTrue(this.turnstile.GetCurrentState() is Unlocked);
Assert.IsTrue(this.controllerSpoof.unlockCalled);
}
[Test]
public void CoinInUnlockedState()
{
this.turnstile.SetState(new Unlocked());
this.turnstile.Coin();
Assert.IsTrue(this.turnstile.GetCurrentState() is Unlocked);
Assert.IsTrue(this.controllerSpoof.thankYouCalled);
}
[Test]
public void InitialConditions() { Assert.IsTrue(this.turnstile.GetCurrentState() is Locked); }
[Test]
public void PassInLockedState()
{
this.turnstile.SetState(new Locked());
this.turnstile.Pass();
Assert.IsTrue(this.turnstile.GetCurrentState() is Locked);
Assert.IsTrue(this.controllerSpoof.alarmCalled);
}
[Test]
public void PassInUnlockedState()
{
this.turnstile.SetState(new Unlocked());
this.turnstile.Pass();
Assert.IsTrue(this.turnstile.GetCurrentState() is Locked);
Assert.IsTrue(this.controllerSpoof.lockCalled);
}
[SetUp]
public void SetUp()
{
this.controllerSpoof = new TurnstileControllerSpoof();
this.turnstile = new TurnstileMachine(this.controllerSpoof);
}
#endregion
private class TurnstileControllerSpoof : ITurnstileController
{
#region Fields
public bool alarmCalled;
public bool lockCalled;
public bool thankYouCalled;
public bool unlockCalled;
#endregion
#region Public Methods and Operators
public void Alarm() { this.alarmCalled = true; }
public void Lock() { this.lockCalled = true; }
public void ThankYou() { this.thankYouCalled = true; }
public void Unlock() { this.unlockCalled = true; }
#endregion
}
}
}
History
- 9/25/13 Initial version.
- 9/25/13 Added T4 template as a text file
A seasoned IT Professional. Programming and data processing artist. Contributor to StackOverflow.