领域模型与状态机

18-02-27 banq
              

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

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

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

该文详细使用了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';
    }
  }
}
<p>

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

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

      |  open               |	held	     |closed
open  |	deposit,withdraw    |	place-hold	 |close
held  |	remove-hold	        | deposit	     |close
closed|	reopen

<p>

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

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
        }
      }
    }
  }
};
<p>

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

如果我们能够发现这个业务服务背后存在对一个统一的状态机(领域模型)进行操作,这些动作只不过是围绕领域模型状态机的行为而已,突出状态,使用状态模式实现,状态模式就是用一个个状态对象替代状态值,比如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;
  }
};
<p>

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

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

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

更多讨论见:

Hackernews讨论1

Hackernews讨论2

              

2