大家好,这里是每周都在陪你进步的网管~!本节我们讲一个行为型的设计模式--状态模式,并通过Golang示例进行实战演示。
状态模式(State Pattern)也叫作状态机模式(State Machine Pattern)状态模式允许对象的内部状态发生改变时,改变它的行为,就好像对象看起来修改了它实例化的类,状态模式是一种对象行为型模式。
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式,把特定于状态的代码抽象到一组独立的状态类中避免过多的状态条件判断,减少维护成本。
状态模式的结构十分简单清晰主要包含三种角色,我们一起来看下。
状态模式的结构如下面的UML类图所示
主要由环境类角色、抽象状态角色和具体状态角色,三个角色构成。
下面举个现实生活中可以用到状态模式的例子,想使用状态模式首先我们得先确定这个业务实体在不同的状态下得拥有不同的行为。
日常生活中常见的拥有状态机的业务实体比如:OA系统的考勤请假审批,每个环节中审批的状态不一样时允许进行的操作也不一样。再比如,大街上的红绿灯,红黄绿不同状态下拍到路上行驶的汽车和检测到车的行驶速度时也会有不同的行为。
下面用 Golang 实现状态模式来解构红绿灯在不同灯的状态下所具有的行为。
首先针对交通红绿灯,每种灯状态下都有亮灯、变灯、测速的行为,所以我们首先定义出交通灯的状态接口。
"本文使用的完整可运行源码
去公众号「网管叨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:
}
}
}
程序编辑后执行,在终端能看到几个灯的状态会循环切换
本文的完整源码,已经同步收录到我整理的电子教程里啦,可向我的公众号「网管叨bi叨」发送关键字【设计模式】领取。
最后我们从适用场景、模式应用和缺点三个方面对状态模式做个总结。
状态模式在工作流或游戏等类型的软件中广泛使用,如在OA办公系统中,一个审批的状态有多种,而且审批状态不同时对批文的操作也有所差异。使用状态模式特别适合用于描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。
说了这么多状态模式的优点,我们最后我们再来说说它的缺点,让我们在实际应用和做系统设计时能更好地做抉择。
扫码关注公众号「网管叨bi叨」
给网管个星标,第一时间吸我的知识 👆
网管整理了一本《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】领取!还有一本《k8s 入门实践》讲解了常用软件在K8s上的部署过程,公众号回复【k8s】即可领取!
觉得有用就点个在看 👇👇👇