领域模型与状态机

日常编程中你使用过状态机吗?也就是状态模式?首先问题是为什么要使用状态机,解答这个问题必须回答如何对抗软件的复杂性?软件的复杂性是因为一份代码做两件事引起的,很多领域模型中都包含一个半残的隐晦的状态机,如果不将状态管理从领域模型中分离出来,相当于让领域模型做两件事,一件事代表实体属性,一件事表达实体当前状态。

大多数领域模型随着时间的推移会有各种不同状态,,围绕这些状态之间的转换方式会有很多业务逻辑代码,如果这些逻辑代码不使用状态模式进行归类统一,会以各种难以理解和难以修改的方式存在各个地方。

通过识别领域模型何时首先被表达为状态机 - 或者识别何时需要将领域模型重构为状态机 - 我们就能保持模型的可理解性和可行性。我们可以驯服他们的复杂性。

该文详细使用了Javascript说明没有使用状态机和使用了状态机在代码质量上的提高。

文中以银行账户为例,说明状态机是响应事件在状态之间的转换,比如银行账户一开始是open状态:

1.在open状态时,银行账户可以响应close事件,并将状态切换到closed状态;

2.当在closed状态时,银行账户会对reopen事件响应,并切换到open状态;

3.当在open状态时,银行账户会响应扣款和存款的事件。

这个切换规则排除了一些改变状态的事件,比如在closed状态时,银行账户就不能响应扣款和存款事件。

对于这段规则,我们如果不挖掘总结业务需求背后的状态模式,而是直接根据功能要求进行设计编码,就会总结出存款deposit 扣款withdraw 放开 关闭等动作,并根据这些动作实现代码如下(通常是服务的接口实现):

let account = {

state: 'open',

balance: 0,

deposit (amount) {

if (this.state === 'open' || this.state === 'held') {

this.balance = this.balance + amount;

} else {

throw 'invalid event';

}

},

withdraw (amount) {

if (this.state === 'open') {

this.balance = this.balance - amount;

} else {

throw 'invalid event';

}

},

placeHold () {

if (this.state === 'open') {

this.state = 'held';

} else {

throw 'invalid event';

}

},

removeHold () {

if (this.state === 'held') {

this.state = 'open';

} else {

throw 'invalid event';

}

},

close () {

if (this.state === 'open' || this.state === 'held') {

if (balance > 0) {

// ...transfer balance to suspension account

}

this.state = 'closed';

} else {

throw 'invalid event';

}

},

reopen () {

if (this.state === 'closed') {

// ...restore balance if applicable

this.state = 'open';

} else {

throw 'invalid event';

}

}

}

如果有新的动作导致新的状态,那么我们就会增加新的方法行为,这样增加下去代码会变得复杂,混沌。

还有一种办法,就是建立一个转换表,把这些动作转换罗列出来:

| open |held |closed

open |deposit,withdraw |place-hold |close

held |remove-hold | deposit |close

closed|reopen

转换表清楚地显示了哪些事件由哪个状态处理,以及它们之间的转换。我们可以将这个想法带到我们的可执行代码中:

const STATES = Symbol("states");

const STARTING_STATE = Symbol("starting-state");

const Account = {

balance: 0,

STARTING_STATE: 'open',

STATES: {

open: {

open: {

deposit (amount) { this.balance = this.balance + amount; },

withdraw (amount) { this.balance = this.balance - amount; },

},

held: {

placeHold () {}

},

closed: {

close () {

if (balance > 0) {

// ...transfer balance to suspension account

}

}

}

},

held: {

open: {

removeHold () {}

},

held: {

deposit (amount) { this.balance = this.balance + amount; }

},

closed: {

close () {

if (balance > 0) {

// ...transfer balance to suspension account

}

}

}

},

closed: {

open: {

reopen () {

// ...restore balance if applicable

}

}

}

}

};

这种方式其实也是不可行,扩展性很差,造成代码非常复杂。

如果我们能够发现这个业务服务背后存在对一个统一的状态机(领域模型)进行操作,这些动作只不过是围绕领域模型状态机的行为而已,突出状态,使用状态模式实现,状态模式就是用一个个状态对象替代状态值,比如open状态使用Open对象表达:

const STATE = Symbol("state");

const STATES = Symbol("states");

const open = {

deposit (amount) { this.balance = this.balance + amount; },

withdraw (amount) { this.balance = this.balance - amount; },

placeHold () {

this[STATE] = this[STATES].held;

},

close () {

if (balance > 0) {

// ...transfer balance to suspension account

}

this[STATE] = this[STATES].closed;

}

};

const held = {

removeHold () {

this[STATE] = this[STATES].open;

},

deposit (amount) { this.balance = this.balance + amount; },

close () {

if (balance > 0) {

// ...transfer balance to suspension account

}

this[STATE] = this[STATES].closed;

}

};

const closed = {

reopen () {

// ...restore balance if applicable

this[STATE] = this[STATES].open;

}

};

上面三个状态对象 open held和closed,每个状态对象里封装了适应他们的动作行为,以及切换规则。

领域模型中封装了业务实体的属性和状态,这已经成为一种通用的设计规则。

当然,反对意见也是有的,他们认为传统的编程语言和命令式思维不适合编写状态机,所以它们没有被广泛使用,这并不令人惊讶。在为什么开发者从来不用状态机中,认为状态机导致复杂,没有什么用处,很多人认为将数据封装在对象中,而无法让数据成为第一公民,同时,状态机让组件复用变得困难,一些人推荐使用ECS系统(实体-组件-系统),让多变的行为和状态成为一个个组件,实体只是标识和具体一些属性,实体与不同组件组合实现不同的系统功能,组合超越继承。

领域模型与状态机

相关推荐