设计模式--状态模式精解
前篇博客写过建造者模式,建造者模式是将实现和显示分离,它属于创建型(Creational)的设计模式,在使用时我们应尽量考虑对象的创建。另一方面它隐藏了具体功能的组装过程,而把具体的实现放在子类中。也就是建造者模式将创建一个产品的基本骨架封装好了,而把具体的步骤放在了子类中,它提高了系统的可扩展性,但却降低了代码的可复用性。这种模式的UML图有两层关系,一层是父类和子类的继承关系,另一层是指挥建造者和具体建造过程的聚合关系。
这两层关系和我们今天要说的状态模式有相似之处,但是它们两个却有着本质的区别,建造者模式是创建型的适用于具体对象或过程的创建,它提高了系统的扩展性,可以构成基本的算法骨架,但是状态模式属于行为型(Behavioural),它侧重于一个过程中对象的行为,反映在类的内部是指类的方法。
下图使用常见的分叉反映了状态模式运行的原理。
上图基本阐释了状态模式的执行过程,蓝色的圆圈表示在执行过程中传入的对象,每个分叉表示的是每个具体子类的状态,在运行时,圆圈的初始状态可以是上面的任意一个分叉,出口是根据自己的执行结果而定,每个状态都会和下面的状态相连,主要找到了自己合适的状态后,才能跳出运行。
一、理论积淀
首先让我们看下状态模式的基本结构图:
结构图的具体代码实现
State:抽象状态类,定义一个借口以封装与Context的一个特性状态相关的行为,是具体状态类的抽象。
''' <summary> ''' 抽象状态类 ''' </summary> ''' <remarks></remarks> Public Class State ''' <summary> ''' 程序抽象运行函数 ''' </summary> ''' <returns></returns> ''' <remarks></remarks> Public Overridable Function Handle(context As Context) As Boolean Return Nothing End Function End Class
ConcreteStateA具体的状态类,实现一个与具体状态相关的行为,在本例中该类状态为初始状态,传入Context中。
''' <summary> ''' 具体的状态A ''' </summary> ''' <remarks></remarks> Public Class ConcreteStateA : Inherits State ''' <summary> ''' 执行状态A的方法,并跳入状态B ''' </summary> ''' <param name="context"></param> ''' <returns></returns> ''' <remarks></remarks> Public Overrides Function Handle(context As Context) As Boolean Console.WriteLine("This is the ConcreteStateA,the Program will jump into ConcreteStateB") '写状态A的方法 context.setState(New ConcreteStateB) '设置新状态,跳入状态B context.Request() '执行状态B的请求 Return True End Function End Class
ConcreteStateB具体的状态类,实现一个与具体状态相关的行为,作为ConcreteStateA类的下一状态,状态A类执行后如果不能满足要求将会跳入该状态,传入Context中。
''' <summary> ''' 具体的状态B ''' </summary> ''' <remarks></remarks> Public Class ConcreteStateB : Inherits State ''' <summary> ''' 执行状态B的方法,并跳入状态C ''' </summary> ''' <param name="context"></param> ''' <returns></returns> ''' <remarks></remarks> Public Overrides Function Handle(context As Context) As Boolean Console.WriteLine("This is the ConcreteStateB,the program will jump into ConcreteStateC.") '写状态B的方法 context.setState(New ConcreteStateC) '设置新状态,跳入状态C context.Request() '执行状态C的方法 Return True End Function End Class
ConcreteStateC,具体的状态C类,实现一个与具体状态相关的行为,作为ConcreteStateB类的下一状态,状态B类执行后如果不能满足要求将会跳入该状态,传入Context中。
''' <summary> ''' 具体的状态C ''' </summary> ''' <remarks></remarks> Public Class ConcreteStateC : Inherits State ''' <summary> ''' 执行状态C的方法 ''' </summary> ''' <param name="context"></param> ''' <returns></returns> ''' <remarks></remarks> Public Overrides Function Handle(context As Context) As Boolean Console.WriteLine("This is the ConcreteStateC,end the program.") Return True End Function End Class
Context类和程序执行入口类,Context和State及子类是一种聚合关系,这种关系属于关联关系的一种,它可理解为has-a的关系,强调集合中有它。而组合关系是一种Contain-a的关系,强调是不可分割的一部分。
''' <summary> ''' 维护一个ConcreteState子类的实例,这个实例定义当前的状态 ''' </summary> ''' <remarks></remarks> Public Class Context Private m_ConcreteState As State '定义初始状态 ''' <summary> ''' 构造函数,传入具体的状态作为程序初始状态 ''' </summary> ''' <param name="ConcreteState"></param> ''' <remarks></remarks> Sub New(ConcreteState As State) m_ConcreteState = ConcreteState End Sub ''' <summary> ''' 状态请求,执行具体状态的方法,并跳入下一状态 ''' </summary> ''' <returns></returns> ''' <remarks></remarks> Public Function Request() As Boolean m_ConcreteState.Handle(Me) '执行相关状态的方法 Return True End Function ''' <summary> ''' 读取程序当前状态,并设置新状态 ''' </summary> ''' <param name="Concretestate"></param> ''' <remarks></remarks> Public Sub setState(Concretestate As State) m_ConcreteState = Concretestate End Sub End Class
程序运行入口,Client类中代码,客户端执行该过程时的执行顺序。
''' <summary> ''' 程序运行入口,客户端执行类 ''' </summary> ''' <remarks></remarks> Public Class Main Sub Main() Dim C As Context '定义程序的执行类 '初始化程序执行类,将初始状态传入其中 '初始状态可以是任意一个ConcreteState C = New Context(New ConcreteStateA) '请求操作,执行状态请求 C.Request() C.Request() C.Request() Console.Read() End Sub End Class
理论精解
状态模式将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个具体状态类中,所以通过定义新的子类可以很容易地增加新的状态和转换。通俗点说,状态模式是为了消除庞大的条件分支语句,通过把判断语句转换到子类中,来减少相互间的依赖,使系统更易于维护。
那么在何时何地使用状态模式呢?我们说状态模式是根据对象的状态来进行的操作,那么在使用它时也离不开对象状态的变化,也就是在复杂的逻辑判断中,需要根据某一对象的各个不同的状态决定运行程序这时考虑使用状态模式。如在系统登陆时,需要首先判断用户名和密码是否存在,然后判断用户是否在异地登陆,是的话要删除异地登陆重新登陆,如果不是的话要执行登陆,这时我们可以考虑使用状态模式,因为登陆操作是根据输入用户名的状态来执行登陆的。
二、实例精华
下面我们以收费系统的登陆为例,来说明状态模式的具体应用。
登陆业务讲解:在输入用户名和密码点击登陆后,首先需要判断用户名是否存在,如果存在的话判断密码是否正确,密码正确后需要判断是否异地登陆,是的话执行退出后登陆,不是的话直接登陆。
上面的登陆逻辑,如果按照我们平常的写法会写一大堆的判断语句,根据填入的信息和已有的信息进行多层判断,这明显增加了代码阅读者的负担,更不利于程序后期的维护。如果在登陆时判断顺序发生更改,就会让我们变得很头疼,要重新组织判断的顺序,但是使用了状态模式后就避免了这种问题,在后期修改后只需重新设定下起始状态和终止状态即可。
收费系统登陆状态模式图:
LoginStateBLL:登陆状态抽象类,将登陆需要执行的方法封装,供子类继承覆盖父类的方法,在具体执行时执行子类中的状态函数。
Option Explicit On Option Strict On Imports BLL.ChargeSystem.BLL Imports Entity.ChargeSystem.Entity Namespace ChargeSystem.BLL ''' <summary> ''' 系统管理员系统登陆类 ''' </summary> Public Class LoginSateBLL ''' <summary> ''' 管理员登陆方法 ''' </summary> ''' <param name="Admin"></param> ''' <param name="LoginBLL ">正上工作用户</param> Public Overridable Function Login(ByVal Admin As AdminEntity, ByVal LoginBLL As LoginChooseBLL) As Boolean Login = False End Function End Class ' LoginSateBLL End Namespace ' BLL
StateTestInfoBLL具体的状态子类,信息检验类,主要负责检验输入的登陆信息,该登陆信息在数据库表中存在的话,将会跳入下一状态,否则退出系统。
''' <summary> ''' 检验输入的信息类 ''' </summary> Public Class SateTestinfoBLL Inherits ChargeSystem.BLL.LoginSateBLL ''' <summary> ''' 管理员登陆方法 ''' </summary> ''' <param name="Admin"></param> ''' <param name="LoginBLL">正上工作用户</param> Public Overrides Function Login(ByVal Admin As AdminEntity, ByVal LoginBLL As LoginChooseBLL) As Boolean Dim DataAccess As New Factory.ChargeSystem.Factory.DataAccess '创建抽象工厂返回实体类 Dim AdminDAL As IAdminIDAL = DataAccess.CreateAdmin() '定义AdminDAL接口,并反射得到AdminDAL Dim enAdmin As AdminEntity = Nothing '定义查询实体类 enAdmin = AdminDAL.SelectAdminUser(Admin) '查询填入的登陆用户是否存在 '判断输入的用户名是否存在 If enAdmin Is Nothing Then Throw New Exception("用户名不存在,请重新输入!") End If '判断输入的密码是否正确 If enAdmin.strPassWord <> Admin.strPassWord Then Throw New Exception("密码输入错误,请重新输入!") End If LoginBLL.SetState(New SateOnworkBLL) '设置下一状态,查询是否正在工作 LoginBLL.LoginSystem(Admin) '执行下一状态的函数 Return True End Function End Class ' SateTestinfoBLL
StateOnworkBLL判断是否异地登陆类,按照传入的用户信息查询是否是异地登陆,如果是的话将会执行异地登陆,删除上次的登陆信息,重新进行登陆。
''' <summary> ''' 管理员登陆 ''' </summary> Public Class SateOnworkBLL Inherits LoginSateBLL ''' <summary> ''' 管理员登陆方法 ''' </summary> ''' <param name="Admin"></param> ''' <param name="LoginBLL">正上工作用户</param> Public Overrides Function Login(ByVal Admin As AdminEntity, ByVal LoginBLL As LoginChooseBLL) As Boolean Dim DataAccess As New Factory.ChargeSystem.Factory.DataAccess '定义抽象工厂类, Dim OnworkDAL As IOnworkIDAL '定义正在工作抽象接口类 Dim WorkDAL As IWorklogIDAL '定义工作记录接口 Dim enOnwork As OnWorkEntity = Nothing '定义判断是否存在用户 OnworkDAL = DataAccess.CreateOnwork() '反射实例化具体的类 enOnwork = OnworkDAL.SelectByUser(Admin) '为正在工作类赋值 '如果用户没有在登陆,执行下一状态 If enOnwork Is Nothing Then LoginBLL.SetState(New SateOutworkBLL) LoginBLL.LoginSystem(Admin) End If '执行登陆,首先退出然后登陆 WorkDAL = DataAccess.CreateWorklog() '创建工作记录类 WorkDAL.InsertLog(enOnwork) '插入工作记录 OnworkDAL.DeleteOnwork(Admin) '删除异地登陆的表 OnworkDAL.InsertOnwork(Admin) '加入本次工作记录 Return True End Function End Class ' SateOnworkBLL
StateOutworkBLL具体的状态子类,执行非异地登陆的登陆方法类。最后一种状态,执行正常登陆。与前面两个子类构成了登陆的三个状态,将复杂的逻辑判断进行了分割,使程序更易于维护。
''' <summary> ''' 正常上机 ''' </summary> Public Class SateOutworkBLL Inherits ChargeSystem.BLL.LoginSateBLL ''' <summary> ''' 管理员登陆方法 ''' </summary> ''' <param name="Admin"></param> ''' <param name="LoginBLL">正上工作用户</param> Public Overrides Function Login(ByVal Admin As AdminEntity, ByVal LoginBLL As LoginChooseBLL) As Boolean Dim DataAccess As New Factory.ChargeSystem.Factory.DataAccess '定义抽象工厂类, Dim OnworkDAL As IOnworkIDAL '定义正在工作抽象接口类 OnworkDAL = DataAccess.CreateOnwork() '反射实例化具体的类 OnworkDAL.InsertOnwork(Admin) '加入本次工作记录 Return True End Function End Class ' SateOutworkBLL
FacadeBLL类,外观类,等同于程序客户端类,实现了程序运行入口的封装,供其它层中调用,封装了具体的实现,降低了客户端和程序的耦合。
Option Explicit On Option Strict On Imports Entity.ChargeSystem.Entity Imports Factory.ChargeSystem.Factory Imports IDAL.ChargeSystem.IDAL Namespace ChargeSystem.BLL ''' <summary> ''' 外观类 ''' </summary> Public Class FacadeBLL ''' <summary> ''' 系统登陆实现过程函数 ''' </summary> ''' <param name="Admin"></param> Public Function LoginAdmin(ByVal Admin As AdminEntity) As AdminEntity Dim LoginChooseBLL As LoginChooseBLL '定义状态模式入口 Dim enAdmin As AdminEntity = Nothing '定义返回值 '实例化入口,并传入初始状态 LoginChooseBLL = New LoginChooseBLL(New SateTestinfoBLL) '实现请求,执行登陆操作 enAdmin = LoginChooseBLL.LoginSystem(Admin) enAdmin = LoginChooseBLL.LoginSystem(Admin) enAdmin = LoginChooseBLL.LoginSystem(Admin) Return enAdmin End Function End Class ' FacadeBLL End Namespace ' BLL
三、对比升华
状态模式和观察者模式
状态模式,顾名思义是指对象的具体状态,在各种不同的状态下拥有不同的行为。它侧重于一个对象的行为状态,在不同的状态下拥有不同的行为,它和观察者模式本质的区别是它是一个对象状态的转换,而观察者模式侧重于相互影响。在上面的登陆例子中自始至终只有一个对象状态的转换,这时不适合使用观察者模式。
状态模式和装饰模式
状态模式中程序的运行是根据选择而定的,当我们将一个对象传入该模式中后,它会在状态之间运行直到找到属于自己的一种状态为止,这种选择循环和装饰模式有点类似。装饰模式也是一种循环关系,但它是一种嵌套循环关系,打个比方就好比人吃东西一样,人吃得越多肚子越大,吃东西就好像是动态的增加类的功能一样,类的功能越来越强大,具体吃东西的顺序和吃不吃都是可以进行选择的。但是状态模式不同,只要我们设置了状态模式的初始状态后,它就会不断的向下循环下去,直到找到属于自己的一种状态为止。
状态模式和策略模式
策略模式实现将算法封装,提高了代码的复用性。在使用时考虑同一业务逻辑在不同的时间应用不同的业务规则,这和登陆状态有相似之处,不同的状态运行不同的结果。那为什么登陆系统不用策略模式?策略模式实现了算法的封装,提高了代码的复用性,但是在登陆逻辑中是为了消除复杂逻辑的判断,并不是选择登陆的策略。
状态模式提高了代码的可理解性,同时也降低了修改的成本,使得同样复杂的逻辑判断阅读起来变得简单,使得程序更易于维护。