背景
企業應用下,需要關注三個狀態機:
- 業務相關的狀態機。
- 審批流程相關的狀態機。
- 持久化相關的狀態機。
某些企業應用開發人員終其一生就是希望能開發出通用的一個框架以簡化這些狀態機的開發。本文重點關注:“業務相關的狀態機”。
常見的狀態機需求
產品的狀態機
單據的狀態機
業務相關的狀態機的一般性需求如下:
- 當處於某個狀態時,可以執行哪些合法的遷移?遷移的前置條件是什么?
- 當處於某個狀態時,可以執行哪些合法的操作?如:已提交和已審核狀態的單據不能被修改。
實現狀態機
我目前使用過兩種思路實現這種狀態機:
- 使用狀態模式。這種要求為每種單據的狀態管理定義一套狀態體系,有點麻煩了。
- 使用狀態表格。這種就是本文介紹的。
下面先看兩個示例。
一個簡單的示例
注意下面的鏈式配置代碼,這些代碼表達的意思是:
In(Status.UnSaved).When(Operation.Save).If(CanSave).TransferTo(Status.Saved)
處於 UnSaved 狀態下,當 Save 操作發生時,如果 CanSave,就遷移到 Saved 狀態。
------------------------------------------------------------------------------------
.In(Status.UnSaved).When(Operation.Edit).Aways().Ok()
處於 UnSaved 狀態下,當 Edit 操作發生時,總是,允許的。
代碼
1 class Order 2 { 3 private readonly StateMachine<Status, Operation> _stateMachine; 4 5 public Status Status { get; internal set; } 6 7 public Order() 8 { 9 _stateMachine = StateMachine<Status, Operation> 10 .Config(() => this.Status, status => this.Status = status) 11 .In(Status.UnSaved).When(Operation.Save).Aways().TransferTo(Status.Saved) 12 .In(Status.Saved).When(Operation.Submit).Aways().TransferTo(Status.Submitted) 13 .Done(); 14 } 15 16 public void Save() 17 { 18 _stateMachine.Schedule(Operation.Save); 19 } 20 21 public void Submit() 22 { 23 _stateMachine.Schedule(Operation.Submit); 24 } 25 26 public void Edit() 27 { 28 _stateMachine.Schedule(Operation.Edit); 29 } 30 }
測試
1 [TestClass] 2 public class StateMachineTest 3 { 4 [TestMethod] 5 public void ValidSave() 6 { 7 var order = new Order { Status = Status.UnSaved }; 8 order.Save(); 9 10 Assert.AreEqual(Status.Saved, order.Status); 11 } 12 13 [TestMethod] 14 [ExpectedException(typeof(StateScheduleException))] 15 public void InvalidSubmit() 16 { 17 var order = new Order { Status = Status.UnSaved }; 18 order.Submit(); 19 } 20 }
一個相對完善的例子
代碼
1 using System; 2 using System.Collections.Generic; 3 using System.Collections.ObjectModel; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 using Happy.Domain; 9 using Happy.StateManager; 10 11 namespace Happy.Examples.ManufactureManagement.Domain.Bases 12 { 13 public abstract class MIAggregateRoot<TItem> : AggregateRoot<Guid> 14 { 15 protected MIAggregateRoot() 16 { 17 this.StateMache = 18 StateMachine<Status, Operation> 19 .Config(() => this.Status, status => this.Status = status) 20 .In(Status.UnSaved).When(Operation.Save).If(CanSave).TransferTo(Status.Saved) 21 .In(Status.UnSaved).When(Operation.Edit).Aways().Ok() 22 .In(Status.Saved).When(Operation.Submit).If(this.CanSubmit).TransferTo(Status.Submitted) 23 .In(Status.Saved).When(Operation.Edit).Aways().Ok() 24 .In(Status.Submitted).When(Operation.Verify).If(this.CanVerify).TransferTo(Status.Verified) 25 .Done(); 26 27 // ReSharper disable DoNotCallOverridableMethodsInConstructor 28 this.Items = new Collection<TItem>(); 29 // ReSharper restore DoNotCallOverridableMethodsInConstructor 30 } 31 32 protected StateMachine<Status, Operation> StateMache { get; private set; } 33 34 protected internal virtual ICollection<TItem> Items { get; protected set; } 35 36 internal Status Status { get; set; } 37 38 protected virtual bool CanSave() 39 { 40 return true; 41 } 42 43 protected virtual bool CanSubmit() 44 { 45 return true; 46 } 47 48 protected virtual bool CanVerify() 49 { 50 return true; 51 } 52 53 internal void Save() 54 { 55 this.StateMache.Schedule(Operation.Save); 56 } 57 58 internal void Submit() 59 { 60 this.StateMache.Schedule(Operation.Submit); 61 } 62 63 internal void Verify() 64 { 65 this.StateMache.Schedule(Operation.Verify); 66 } 67 68 internal void AddItem(TItem item) 69 { 70 this.AddItems(new List<TItem> { item }); 71 } 72 73 internal void AddItems(IEnumerable<TItem> items) 74 { 75 this.StateMache.Schedule(Operation.Edit); 76 77 foreach (var item in items) 78 { 79 this.Items.Add(item); 80 } 81 } 82 83 internal void DeleteItem(TItem item) 84 { 85 this.DeleteItems(new List<TItem> { item }); 86 } 87 88 internal void DeleteItems(IEnumerable<TItem> items) 89 { 90 this.StateMache.Schedule(Operation.Edit); 91 92 foreach (var item in items) 93 { 94 this.Items.Remove(item); 95 } 96 } 97 } 98 }
測試
1 using System; 2 using Microsoft.VisualStudio.TestTools.UnitTesting; 3 4 using Happy.StateManager; 5 using Happy.Examples.ManufactureManagement.Domain.Bases; 6 using Happy.Examples.ManufactureManagement.Domain.QualityTests; 7 8 namespace Happy.Examples.ManufactureManagement.Domain.Test.QualityTests 9 { 10 [TestClass] 11 public class QualityTestTest 12 { 13 [TestMethod] 14 public void TestUnSavedQualityTest() 15 { 16 var entity = this.MockUnQualityTest(Status.UnSaved); 17 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id)); 18 entity.Save(); 19 20 Assert.AreEqual(Status.Saved, entity.Status); 21 } 22 23 [TestMethod] 24 public void TestSavedQualityTest() 25 { 26 var entity = this.MockUnQualityTest(Status.Saved); 27 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id)); 28 entity.Submit(); 29 30 Assert.AreEqual(Status.Submitted, entity.Status); 31 } 32 33 [TestMethod] 34 [ExpectedException(typeof(StateScheduleException))] 35 public void TestSubmittedQualityTest() 36 { 37 var entity = this.MockUnQualityTest(Status.Submitted); 38 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id)); 39 } 40 41 private QualityTest MockUnQualityTest(Status status) 42 { 43 return new QualityTest 44 { 45 Id = Guid.NewGuid(), 46 Status = status 47 }; 48 } 49 } 50 }
實現代碼
代碼
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.ExtentionMethods; 8 using Happy.StateManager.Configuration; 9 10 namespace Happy.StateManager 11 { 12 /// <summary> 13 /// 狀態機。 14 /// </summary> 15 public sealed class StateMachine<TState, TOperation> 16 { 17 private readonly List<Transition<TState, TOperation>> _transitions = new List<Transition<TState, TOperation>>(); 18 private readonly Func<TState> _stateGetter; 19 private readonly Action<TState> _stateSetter; 20 21 /// <summary> 22 /// 構造方法。 23 /// </summary> 24 public StateMachine(Func<TState> stateGetter, Action<TState> stateSetter) 25 { 26 stateGetter.MustNotNull("stateGetter"); 27 stateSetter.MustNotNull("stateSetter"); 28 29 _stateGetter = stateGetter; 30 _stateSetter = stateSetter; 31 } 32 33 /// <summary> 34 /// 配置狀態機。 35 /// </summary> 36 public static IConfig<TState, TOperation> Config(Func<TState> stateGetter, Action<TState> stateSetter) 37 { 38 stateGetter.MustNotNull("stateGetter"); 39 stateSetter.MustNotNull("stateSetter"); 40 41 return new Config<TState, TOperation>(stateGetter, stateSetter); 42 } 43 44 /// <summary> 45 /// 配置狀態遷移。 46 /// </summary> 47 public StateMachine<TState, TOperation> ConfigTransition( 48 TState sourceState, 49 TOperation operation, 50 ICondition condition, 51 TState targetState) 52 { 53 sourceState.MustNotNull("sourceState"); 54 operation.MustNotNull("operation"); 55 condition.MustNotNull("condition"); 56 targetState.MustNotNull("targetState"); 57 58 var transition = new Transition<TState, TOperation>( 59 sourceState, 60 operation, 61 condition, 62 targetState); 63 64 _transitions.Add(transition); 65 66 return this; 67 } 68 69 /// <summary> 70 /// 使用<paramref name="operation"/>調度狀態機。 71 /// </summary> 72 public void Schedule(TOperation operation) 73 { 74 operation.MustNotNull("operation"); 75 76 var currentState = _stateGetter(); 77 var transition = _transitions 78 .FirstOrDefault(x => 79 x.SourceState.Equals(currentState) 80 && 81 x.Operation.Equals(operation) 82 && 83 x.Condition.IsSatisfied()); 84 85 if (transition == null) 86 { 87 throw new StateScheduleException(currentState, operation); 88 } 89 90 _stateSetter(transition.TargetState); 91 } 92 } 93 }
說明
內部就是一個狀態表格,沒啥交代的,有興趣的朋友可以去 http://happy.codeplex.com/SourceControl/latest,找到 Happyframework/Src/Happy.StateManager 下載最新代碼看看。
進一步交代的問題
如果需要在真實的項目中使用這個模式,有兩個問題還需要解決:
第一個問題:遷移的前置條件判斷和后置操作的執行如果需要更多的信息,而這些信息不在實體內,怎么辦?處理這個問題有很多種方式,這里介紹一下我目前最偏好的一種,引入領域服務:
領域服務代碼
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.Examples.ManufactureManagement.Domain.QualityTests; 8 using Happy.Examples.ManufactureManagement.Domain.SmallCuts; 9 10 namespace Happy.Examples.ManufactureManagement.Domain.Services 11 { 12 public sealed class QualityTestManager 13 { 14 private readonly ISmallCutRepository _smallCutRepository; 15 16 public QualityTestManager(ISmallCutRepository smallCutRepository) 17 { 18 _smallCutRepository = smallCutRepository; 19 } 20 21 public void Save( 22 QualityTest qualityTest, 23 IEnumerable<QualityTestItem> addedItems, 24 IEnumerable<QualityTestItem> deletedItems) 25 { 26 qualityTest.AddItems(addedItems); 27 qualityTest.DeleteItems(deletedItems); 28 29 qualityTest.Save(); 30 31 this.AcquireLocks(qualityTest, addedItems); 32 this.ReleaseLocks(qualityTest, deletedItems); 33 } 34 35 public void Submit( 36 QualityTest qualityTest, 37 IEnumerable<QualityTestItem> addedItems, 38 IEnumerable<QualityTestItem> deletedItems) 39 { 40 qualityTest.AddItems(addedItems); 41 qualityTest.DeleteItems(deletedItems); 42 43 qualityTest.Submit(); 44 45 this.AcquireLocks(qualityTest, addedItems); 46 this.ReleaseLocks(qualityTest, deletedItems); 47 } 48 49 public void Verify(QualityTest qualityTest) 50 { 51 qualityTest.Verify(); 52 53 foreach (var item in qualityTest.Items) 54 { 55 var smallCut = _smallCutRepository.Load(item.SmallCutId); 56 smallCut.ReleaseLock(CreateLockInfo(qualityTest)); 57 } 58 } 59 60 private void AcquireLocks(QualityTest qualityTest, IEnumerable<QualityTestItem> addedItems) 61 { 62 foreach (var item in addedItems) 63 { 64 var smallCut = _smallCutRepository.Load(item.SmallCutId); 65 smallCut.AcquireLock(CreateLockInfo(qualityTest)); 66 } 67 } 68 69 private void ReleaseLocks(QualityTest qualityTest, IEnumerable<QualityTestItem> deletedItems) 70 { 71 foreach (var item in deletedItems) 72 { 73 var smallCut = _smallCutRepository.Load(item.SmallCutId); 74 smallCut.ReleaseLock(CreateLockInfo(qualityTest)); 75 } 76 } 77 78 private static LockInfo CreateLockInfo(QualityTest qualityTest) 79 { 80 return new LockInfo(LockType.LockByQualityTest, qualityTest.Id); 81 } 82 } 83 }
第二個問題:之前我只需要在 UI 中控制好這種狀態機就行了,如果移動到了領域層,UI 也要重復一遍了,如何消除這種重復,答案是:引入元編程,讓 UI 能自動識別這些元數據,最小化重復,這里就不給出實現(還沒做)。
備注
上面狀態機的配置過程也很有意思,In后只能是When,When后可以是If或Always,有點類似語法樹了,找個機會可以寫篇文章(實現是很簡單的)。