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

  • Post author:
  • Post category:其他


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

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

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

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

状态模式的构成

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

b85eccec911a63bf312eb19ab177c175.png

状态模式 — UML类图

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

  • Context(环境类):环境类又称为上下文类,它定义客户端需要的接口,内部维护一个当前状态实例,并负责具体状态的切换。

  • State(抽象状态):定义状态下的行为,可以有一个或多个行为。

  • ConcreteState(具体状态):每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。

状态模式实战

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

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

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

ce4e7e6ec2906a87cea1c5962da8bf39.png
首先针对交通红绿灯,每种灯状态下都有亮灯、变灯、测速的行为,所以我们首先定义出交通灯的状态接口。

"本文使用的完整可运行源码
去公众号「网管叨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:
  }
 }
}

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

acee6464d56e47b7bfbb95d2d674c7c6.png

运行结果

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

376e82f9e079a4fb20f9cb964f5f4721.png

公众号「网管叨bi叨」发送关键字【设计模式】领取。

总结

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

适用场景


  • 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。

    • 模式将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。


  • 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。

    • 状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 通过业务逻辑内聚,减少客户端类的这部分工作。


  • 当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。

    • 状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。

模式应用

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

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

缺点

  • 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。

  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。

  • 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

参考链接

  • https://medium.com/swlh/how-the-state-pattern-will-improve-your-go-programs-e9babeed6e41

  • https://github.com/Cyberlurk/trafficlight

– END –

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 👆

网管整理了一本《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】领取!还有一本《k8s 入门实践》讲解了常用软件在K8s上的部署过程,公众号回复【k8s】即可领取!

觉得有用就点个在看  👇👇👇



版权声明:本文为kevin_tech原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。