likes
comments
collection
share

Go设计模式实战--用状态模式实现系统工作流和状态机

作者站长头像
站长
· 阅读数 24

大家好,这里是每周都在陪你进步的网管~!本节我们将讲解状态模式这一设计模式,并通过golang示例进行实战演示。

状态模式(State Pattern)也叫作状态机模式(State Machine Pattern)状态模式允许对象的内部状态发生改变时,改变它的行为,就好像对象看起来修改了它实例化的类,状态模式是一种对象行为型模式。

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式,把特定于状态的代码抽象到一组独立的状态类中避免过多的状态条件判断,减少维护成本。

状态模式的结构十分简单清晰主要包含三种角色,我们一起来看下。

状态模式的构成

状态模式的结构如下面的UML类图所示

Go设计模式实战--用状态模式实现系统工作流和状态机

主要由环境类角色、抽象状态角色和具体状态角色,三个角色构成。

  • ontext(环境类):环境类又称为上下文类,它定义客户端需要的接口,内部维护一个当前状态实例,并负责具体状态的切换。
  • State(抽象状态):定义状态下的行为,可以有一个或多个行为。
  • ConcreteState(具体状态):每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。

状态模式实战

下面举个现实生活中可以用到状态模式的例子,想使用状态模式首先我们得先确定这个业务实体在不同的状态下得拥有不同的行为。

日常生活中常见的拥有状态机的业务实体比如:OA系统的考勤请假审批,请不同的假他们的流程不一样,每个环节中审批的状态不一样时允许进行的操作也不一样。再比如,大街上的红绿灯,红黄绿不同状态下拍到路上行驶的汽车和检测到车的行驶速度时也会有不同的行为。

下面用 golang 实现状态模式来解构红绿灯在不同灯的状态下所具有的行为。

Go设计模式实战--用状态模式实现系统工作流和状态机 首先针对交通红绿灯,每种灯状态下都有亮灯、变灯、测速的行为,所以我们首先定义出交通灯的状态接口。

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
// State interface
type LightState interface {
	// 亮起当前状态的交通灯
	Light()
	// 转换到新状态的时候,调用的方法
	EnterState()
	// 设置一个状态要转变的状态
	NextLight(light *TrafficLight)
	// 检测车速
	CarPassingSpeed(*TrafficLight, int, string)
}

然后我们定义环境类 Context,它提供客户端调用状态行为的接口。

// Context
type TrafficLight struct {
	State LightState
	SpeedLimit int
}

func NewSimpleTrafficLight(speedLimit int) *TrafficLight {
	return &TrafficLight{
		SpeedLimit: speedLimit,
		State: NewRedState(),
	}
}

接下来我们在实现具体状态前,先定义一个DefaultLightState类型用于让具体的LightState 嵌套组合,减少公用法在每个具体 LightState 实现类中的重复实现。

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
type DefaultLightState struct {
	StateName string
}

func (state *DefaultLightState) CarPassingSpeed(road *TrafficLight, speed int, licensePlate string) {
	if speed > road.SpeedLimit {
		fmt.Printf("Car with license %s was speeding\n", licensePlate)
	}
}

func (state *DefaultLightState) EnterState(){
	fmt.Println("changed state to:", state.StateName)
}

func (tl *TrafficLight) TransitionState(newState LightState) {
	tl.State = newState
	tl.State.EnterState()
}

这个技巧我们也在模版和策略模式使用过,诀窍是它只实现LightState里的通用方法的默认版,不能实现所有的方法,那样的话他也就算一个 LightState 具体实现了,而这不是我们想要的,我们想要的是把接口中每个类型实现逻辑不同的关键方法以及覆盖默认版的通用方法的工作,留给具体类型去实现。

接下来我们定义三个具体状态类型,去实现LightState接口,首先是红灯的状态实现。

// 红灯状态
type redState struct {
	DefaultLightState
}

func NewRedState() *redState {
	state := &redState{}
	state.StateName = "RED"
	return state
}

func (state *redState) Light() {
	fmt.Println("红灯亮起,不可行驶")
}

func (state *redState) CarPassingSpeed(light *TrafficLight, speed int, licensePlate string) {
	// 红灯时不能行驶, 所以这里要重写覆盖 DefaultLightState 里定义的这个方法
	if speed > 0 {
		fmt.Printf("Car with license \"%s\" ran a red light!\n", licensePlate)
	}
}

func (state *redState) NextLight(light *TrafficLight){
	light.TransitionState(NewGreenState())
}

红灯的时候不能行使,所以这里要重写覆盖DefaultLightState 里定义的CarPassingSpeed方法。

接下来是绿灯和黄灯状态:

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
// 绿灯状态
type greenState struct{
	DefaultLightState
}

func NewGreenState() *greenState{
	state :=  &greenState{}
	state.StateName = "GREEN"
	return state
}

func (state *greenState) Light(){
	fmt.Println("绿灯亮起,请行驶")
}

func (state *greenState) NextLight(light *TrafficLight){
	light.TransitionState(NewAmberState())
}

// 黄灯状态
type amberState struct {
	DefaultLightState
}

func NewAmberState() *amberState{
	state :=  &amberState{}
	state.StateName = "AMBER"
	return state
}

func (state *amberState) Light(){
	fmt.Println("黄灯亮起,请注意")
}

func (state *amberState) NextLight(light *TrafficLight){
	light.TransitionState(NewRedState())
}

通过上面的代码我们可以看到状态实现类在内部确定了状态可以转换的下个状态,这样就把系统流程的状态机留在了内部,避免让客户端代码再去做状态链初始化和转换的判断,符合高内聚的设计原则,从而解放了客户端。

func main() {
	trafficLight := NewSimpleTrafficLight(500)

	interval := time.NewTicker(5 * time.Second)
	for {
		select {
			case <- interval.C:
				trafficLight.State.Light()
				trafficLight.State.CarPassingSpeed(trafficLight, 25, "CN1024")
				trafficLight.State.NextLight(trafficLight)
		default:
		}
	}
}

程序编辑后执行,在终端能看到几个灯的状态会循环切换

Go设计模式实战--用状态模式实现系统工作流和状态机

本文的完整源码,已经同步收录到我整理的电子教程里啦,可向我的公众号「网管叨bi叨」发送关键字【设计模式】领取。

Go设计模式实战--用状态模式实现系统工作流和状态机

总结

最后我们从适用场景、模式应用和缺点三个方面对状态模式做个总结。

适用场景

  • 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
    • 模式将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。
  • 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。
    • 状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 通过业务逻辑内聚,减少客户端类的这部分工作。
  • 当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
    • 状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。

模式应用

状态模式在工作流或游戏等类型的软件中广泛使用,如在OA办公系统中,一个审批的状态有多种,而且审批状态不同时对批文的操作也有所差异。使用状态模式特别适合用于描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。

说了这么多状态模式的优点,我们最后我们再来说说它的缺点,让我们在实际应用和做系统设计时能更好地做抉择。

缺点

  • 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
  • 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

参考链接