说到状态模式,如果你看过之前发布的重构系列的文章中的《代码重构(六):代码重构完整案例》这篇博客的话,那么你应该对“状态模式”并不陌生,因为我们之前使用到了状态模式进行重构。上一篇博客我们讲的主题是“组合模式”,我们使用组合模式创建了一个树形结构,并给出了遍历方式。今天我们来认识一下另一种模式,那就是“状态模式”,今天就从银行的ATM自动取款机中的取款流程来学习一下状态模式。
还是老规矩,开门见山。下方是状态模式的定义:
状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修够了它的类。
其实状态模式与策略模式的共同点非常之多,可以说状态模式是策略模式的升级版本。关于策略模式的内容,请参加之前关于策略模式的博客《设计模式(一):“穿越火线”中的“策略模式”(Strategy Pattern)》。今天我们就从取款机中的各种状态,以及各种状态间的切换来学习一下状态模式。我们先给出没有状态模式的实现方案,然后根据其产生的问题类使用状态模式进行重构。废话少说,进入今天的主题。
一、无“状态模式”的ATM
该部分给出了无“状态模式”的ATM机的具体实现,该部分先对各种状态已经各种状态间的转换进行分析,然后给出个个状态之间的关系,有点状态图的意思。然后在根据此状态图来实现我们的代码,当然虽然是根据状态图实现的代码,在该部分我们没有使用状态模式。所有的状态转换我们都在一个ATM的类中进行的。该部分就给出了具体实现。
1.ATM机状态分析
首先我们先对ATM机的各种状态,下方就是我们所画出的类“状态图”。每个方框就是一种状态,而我们的ATM机大致分为无卡,有卡,解密,取款,取出金额,余额不足这六种状态,也就是下方“状态图”中方框中的内容。然后就是动作了,每种状态间的转化我们需要动作才可以完成。在我们的ATM机中大致分为插卡,退卡,输入密码,输入金额,确认取款这几种动作。状态间转换时所需的动作关系如下所示。因为该部分实现的代码位于一个类中,再扯就不做过多的赘述了。
2. 上述状态关系的具体实现
在代码实现时,首先我们使用枚举来列举出所有的状态,此处我们命名为ATMState。下方代码段就是我们ATM机所有的状态,如下所示:
给出状态枚举后,接着我们要实现ATM机的类,下方就是我们ATM机的类。state成员变量就记录了当前ATM机所处的状态,默认是无卡状态。money成员变量记录了当前取款机中银行卡的余额,余额默认是0。inputMoney存储了用户想提取金额,默认值也是0。insertBankCard()方法则表示插入银行卡的动作,在执行该动作时,根据ATM机当前所处的状态来决定要做哪些事情。比如当前已经处于有卡的状态(HasBankCardState),则会提示“目前已有银行卡,可以输入密码进行取款”,如处于无卡状态(NoBankCardState),则可以插入银行卡,并将状态改为有卡状态(HasBankCardState),具体请看下方该方法的实现。backBankCard()方法则代表着退卡的动作,在该方法的实现方式与insertBankCard()方法类似,也是根据不同的ATM状态,执行不同的事情,具体实现如下所示。inputPassword()则表示输入密码的动作,inputMoney(money)则代表着输入取款金额的动作。tapOkButton()方法则代表着点击取款按钮的动作。
这些动作的实现方式都差不多,都是根据当前ATM机的状态来执行一些东西。如果所做的事情会改变ATM机的状态(比如插入卡),那么在执行完动作后就立即改变ATM机的状态。下方给出了两个方法的实现,其他的方法请参考Github分享的完整实例,分享地址见博文结尾部分。
3、测试用例
上面我们给出了ATM机的实现,接下来就是到了测试的时候了,也就是ATM机我们造完了,接下来该使用了。下方就是我们的测试用例,该用例稍稍的有些复杂。首先我们先创建了一个ATM机的对象,然后在无卡状态下插入银行卡,紧接着在有卡状态下有插入一张银行卡(这个肯定会插入失败的)。然后在有卡状态下输入密码、输入正确的金额取款。取款成功后再次输入密码和大笔金额(肯定提示余额不足),然后在余额不足的状态下进行退卡。测试用例具体如下:
上面测试用例运行结果如下。从下方的输出结果中我们可以看出,无卡状态下插入卡会从无卡状态变为有卡状态。然后在有卡状态下再次插入银行卡会提示“目前已有银行卡,可以输入密码进行取款”。有卡状态下我们可以进行密码输入,金额输入,进行取款。取款成功可以再次输入密码进行二次取款,取款时如果余额不足,那么会成为余额不足状态。在余额不足状态时可以进行再次取款或者退出银行卡。说这么多,都是对上面状态图的反应。
二、使用“状态模式”重构
上述代码虽然能运行,但是问题是非常多的。且不说素有的东西都堆在ATM()这个类中,如果我们添加了一种状态,那么上面所提到的五个方法都得改变,这显然是不行的,在此是五个方法,加入是10个,二十个呢,就没法搞了。所以我们要换一种思路来解决这个问题。那么就是使用“状态模式”。经过我们的分析,状态有可能会改变,所以我们要讲变化的放在一块不变的放在一块。可上面那种设计方式不好将变化的部分进行提取,因为上面代码是动作中含有各种状态。
我们换一种思路,就是将状态含有动作。也就是说一种状态下有各种操作,而不是上面的一种动作中有各种状态。这个思路如果能转变过来,那么我们的状态模式就好理解多了。如果状态下含有各种动作的话,当新增一种状态时只需要将该状态包括这些动作即可,而不影响其他的状态。而最初的实现方式新增一种状态则需要修改每个动作的内容。接下来我们就是要实现“状态包含不同的动作”,在状态执行动作时,会根据该状态下的该动作来对ATM机的当前状态进行修改,也就是引入“状态模式”。具体实现方式如下:
1.“类图”的设计
因为引入“状态模式”后,我们的ATM机稍微有些复杂,所以在此我们给出了类图的设计。下方就是我们将要使用代码进行实现的类图。要想实现“状态包含个个动作”的最好的方式就是为每个状态声明一个类,然后在类中实现该状态下的不同的操作。因为我们要依赖接口编程,而不依赖实现编程。所以我们创建了两个接口,一个是状态接口StateType,另一个则是ATM机接口ATMType。在StateType中声明了状态要包含的动作,因为这些动作也是ATM机的动作,所以我们的ATMType接口也遵循StateType接口。这也等同于ATMType中同样声明了这些动作,这些动作也是ATM机必须实现的。ATM机的类则遵循与ATMType协议。BaseStateClass是所有状态的基类,该状态的基类依赖于ATMType接口,因为在状态中执行一些动作时会修改ATM机的状态,所以BaseStateClass会依赖于ATMType协议。
所有的状态类都继承自BaseStateClass,同样给出了该状态下的一些特定动作。在一些状态下执行一些动作时会修改ATM的动作。比如在ATM无卡状态下调用插卡动作(insertBankCard())方法,那么ATM机的状态就会懂无卡状态变为有卡状态。ATM机类的动作是依赖于状态的,所以ATM机依赖于状态的接口,而不依赖于状态的具体实现。下方就是引入状态模式的类图。
2. 代码实现
有了类图在给出代码实现则简单许多,首先我们会相关的接口和基类。因为我们要面向接口编程,而不是面向实现编程,所以在程序设计之初我们要先定义接口。下方就是我们定义的StateType接口和ATMType接口,以及BaseStateClass基类。StateType接口定义了所有具体的状态必须要实现的方法。而ATMType协议继承自StateType,在StateType的基础上添加了ATM机特有的方法,ATMType中声明的方法也是ATM具体实现类中必须要实现的方法。代码实现具体如下所示:
下方具体给出了无卡状态类的实现方式,在无卡状态下如果你插入银行卡,也就是调用insertBankCard()方法,那么ATM机的状态就会改变成“有卡状态”,具体实现方式如下。在无卡状态下,调用insertBankCard()以外的方法则不会改变ATM()机的状态。其余的状态的实现方式与无卡状态类的实现方式类似,就是在合适的动作中改变ATM机对象的状态。因篇幅有限,在本篇博客中就不给出具体实现了,具体实现请参加github上分享的完整实例。
接下来就是要重构我们的ATM机类了,ATM类要遵循ATMType协议。其中的stateObject成员变量的类型就是stateType类型的对象,该对象就是ATM机当前所处的状态对象,该状态对象模式是NoBankCardState类的对象。在ATM类中的动作是调用stateObject对象中相应的方法,因为状态对象中已经封装了该状态下所有的动作,所以在ATM类中直接调用即可。如果ATM机状态被改变了,那么stateObject所执行的动作也会不同,这也就是常说的多态了。下方的changeState()方法其实可以提取出类封成一个简单工厂的,因为在此我们的主题是“状态模式”,所以在此就没有进行封装。ATM类的具体实现方式如下。
3、测试用例
重构后的测试用例参照上一部分的测试用例,因为对外的类名以及动作没有发生变化,所以之前的测试用例还是可以使用的。这也就是我们之前所说的重构的魅力,所以在此的测试用例就省略了。
至此,我们的状态模式就介绍完了。状态模式其实就是封装了基于状态的行为,并将行为委托到当前状态中。有时候换一种思路会起到不一样的效果。第一部分是动作包含各种状态,而重构时我们使用了状态包括不同动作的方式引入了“状态模式”。因为博客中的代码是部分代码,完整实例请看github上分享的代码.
上述代码gitHub分享地址为:https://github.com/lizelu/DesignPatterns-Swift