“有限状态元为软件编写中最有用的抽象并得到广泛应用。它们提供一种简洁优雅的方式来探索和定义复杂系统的行为。它们同时提供一种强大的实施策略,易懂且易改。” Martin, Micah; Martin, Robert C. (2006-07-20)(作者书中所言),源于《C#的敏捷原则,模式与实践》
构造一个有限状态机有许多种方法,我非常喜欢书中所演示的优雅方法。然而,它仍然需要我在状态机以外提前编写许多类。虽然在许多情况下这是可以接受的,但在我的项目中不能这样。我需要载入一些类型并且动态编译,为此我略微修改了一下书中的例子,让所有相关类都存在于同一命名空间下成为可能。这将导致部分重复代码,但对我而言这样的风险可以接受。
代码
为了使用T4模板中的对象初始化器和LINQ查询,我决定首先创建一些帮助类:
namespace FiniteStateMachine { publicclass FSMMachine { publicstring Name { get; set; } publicstring InitialState { get; set; } public FSMState[] States { get; set; } } publicclass FSMState { publicstring Name { get; set; } public FSMEvent[] Events { get; set; } } publicclass FSMEvent { publicstring Name { get; set; } publicstring NewState { get; set; } publicstring Action { get; set; } } }
其实非常简单:
FSMMachine 包含了我状态机中的基本定义如在 Name and InitialState, 增加了允许状态States的数组
FSMState 有 Name 属性,和一些事件
FSMEvent 有 Name, NewState 和 Action 属性
为了定义T4模板中的状态机,我简单使用如下代码:
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();
这与书中的代码非常类似.
FSMName Turnstile Context TurnstileActions Initial Locked Exception FSMError { Locked { Coin Unlocked Unlock Pass Locked Alarm } Unlocked { Coin Unlocked Thankyou Pass Locked Lock } }
由于我的上下文对象在生成的命名空间中是隔离的,我无需在FSMMachine 中创建特别的属性。类似的,所有生成命名空间也有其自己的FSMError代码。
你注意我所有的状态,事件和响应代码都定义为常量字符串。我发现使用常量定义在避免输入错误和合理化状态机定义上更为方便。
当你保存T4模板后,内置的客制工具开始生成由T4状态定义的输出。
例如T4模板如下:
<#@ template hostspecific="false" language="C#" #> <#@ output extension=".txt" #> Hello, world!
将会导致创建一个文本文件,此文件拥有模板相同的文件名,并以单行输出“Hello, world!”。有许多构造函数能在T4模板中使用,包括LINQ和任何你自己的类库。为了导入程序集,你必须使用:
<#@ assembly name="System.Core" #> <#@ assembly name="..\FSM.dll" #>
这将引用System.Core库,我们起初创建的帮助类也将引入。
然后,正如在C#中一样,你必须在自己的模板中定义命名空间:
<#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="FiniteStateMachine" #> <#@ output extension=".cs" #>
现在你已经准备好使用所有可能的代码生成一个IController接口,模板将如下:
publicinterface I<#= stateMachine.Name #>Controller { <# foreach(string act in actions){#> void <#= act #>(); <# } #> }
上述的状态机定义将生成如下的接口:
public interface ITurnstileController { void Unlock(); void Alarm(); void ThankYou(); void Lock(); }
这些步骤可以重复许多遍,使用C#语言的充分支持,来生成一行又一行的C#代码,而不用人工输入代码。
我已经将FSM帮助类,FSM T4模板以及生成状态机代码的例子程序都打包上传。
这个技术将为我节省大量的时间,我希望对你也能有所帮助。这里是测试用例,一个从书中略微修改的简单例子,用来验证生成的状态机代码一如设计目的。
namespace FSMTests { using NUnit.Framework; using TurnstileMachine; [TestFixture] publicclass TTTurnstileTests { #region Fields private TurnstileControllerSpoof controllerSpoof; private TurnstileMachine turnstile; #endregion#region Public Methods and Operators [Test] publicvoid CoinInLockedState() { this.turnstile.SetState(new Locked()); this.turnstile.Coin(); Assert.IsTrue(this.turnstile.GetCurrentState() is Unlocked); Assert.IsTrue(this.controllerSpoof.unlockCalled); } [Test] publicvoid CoinInUnlockedState() { this.turnstile.SetState(new Unlocked()); this.turnstile.Coin(); Assert.IsTrue(this.turnstile.GetCurrentState() is Unlocked); Assert.IsTrue(this.controllerSpoof.thankYouCalled); } [Test] publicvoid InitialConditions() { Assert.IsTrue(this.turnstile.GetCurrentState() is Locked); } [Test] publicvoid PassInLockedState() { this.turnstile.SetState(new Locked()); this.turnstile.Pass(); Assert.IsTrue(this.turnstile.GetCurrentState() is Locked); Assert.IsTrue(this.controllerSpoof.alarmCalled); } [Test] publicvoid PassInUnlockedState() { this.turnstile.SetState(new Unlocked()); this.turnstile.Pass(); Assert.IsTrue(this.turnstile.GetCurrentState() is Locked); Assert.IsTrue(this.controllerSpoof.lockCalled); } [SetUp] publicvoid SetUp() { this.controllerSpoof = new TurnstileControllerSpoof(); this.turnstile = new TurnstileMachine(this.controllerSpoof); } #endregionprivateclass TurnstileControllerSpoof : ITurnstileController { #region Fields publicbool alarmCalled; publicbool lockCalled; publicbool thankYouCalled; publicbool unlockCalled; #endregion#region Public Methods and Operators publicvoid Alarm() { this.alarmCalled = true; } publicvoid Lock() { this.lockCalled = true; } publicvoid ThankYou() { this.thankYouCalled = true; } publicvoid Unlock() { this.unlockCalled = true; } #endregion } } }