企业应用:一个够用的、通用的状态机(管理实体的业务状态)
背景
企业应用下,需要关注三个状态机:
- 业务相关的状态机。
- 审批流程相关的状态机。
- 持久化相关的状态机。
某些企业应用开发人员终其一生就是希望能开发出通用的一个框架以简化这些状态机的开发。本文重点关注:“业务相关的状态机”。
常见的状态机需求
产品的状态机
单据的状态机
业务相关的状态机的一般性需求如下:
- 当处于某个状态时,可以执行哪些合法的迁移?迁移的前置条件是什么?
- 当处于某个状态时,可以执行哪些合法的操作?如:已提交和已审核状态的单据不能被修改。
实现状态机
我目前使用过两种思路实现这种状态机:
- 使用状态模式。这种要求为每种单据的状态管理定义一套状态体系,有点麻烦了。
- 使用状态表格。这种就是本文介绍的。
下面先看两个示例。
一个简单的示例
注意下面的链式配置代码,这些代码表达的意思是:
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,有点类似语法树了,找个机会可以写篇文章(实现是很简单的)。