思考并回答以下问题:
工作状态-函数版
主程序如下:
1 | class Program |
应该有个“工作”类,‘写程序’方法是类方法,而‘钟点’、‘任务完成’其实就是类的对外属性
工作状态-分类版
工作类
1 | // 工作 |
客户端程序如下:
1 | class Program |
结果表现如下
1 | 当前时间:9点 上午工作,精神百倍 |
方法过长是坏味道
MartinFowler曾在《重构》中写过一个很重要的代码坏味道,叫做‘Long Method’,方法如果过长其实极有可能是有坏味道了。
‘Work(工作)’类的‘WriteProgram(写程序)’方法过长。这个方法很长,而且有很多的判断分支,这也就意味着它的责任过大了。无论是任何状态,都需要通过它来改变,这实际上是很糟糕的。
面向对象设计其实就是希望做到代码的责任分解。这个类违背了‘单一职责原则’。
由于‘WriteProgram(写程序)’的方法里有这么多判断,使得任何需求的改动或增加,都需要去更改这个方法了,比如,你们老板也感觉加班有些过分,对于公司的办公室管理以及员工的安全都不利,于是发了一通知,不管任务再多,员工必须在20点之前离开公司。这样的需求很合常理,所以要满足需求你就得更改这个方法,但真正要更改的地方只涉及到17点到22点之间的状态,但目前的代码却是对整个方法做改动的,维护出错的风险很大。
这样写方法违背了‘开放-封闭原则’。
把这些分支想办法变成一个又一个的类,增加时不会影响其他类。然后状态的变化在各自的类中完成。
GoF已经为这类问题提供了解决方案,那就是‘状态模式’。
状态模式
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,那就没必要用‘状态模式’了。”
状态模式(State)结构图
State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
1 | abstract class State |
ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。
1 | class ConcreteStateA : State |
Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。
1 | class Context |
客户端代码
1 | class Program |
状态模式好处与用处
状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
是不是就是将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换。
这样做的目的就是为了消除庞大的条件分支语句,大的分支判断会使得它们难以修改和扩展,就像我们最早说的刻版印刷一样,任何改动和变化都是致命的。状态模式通过把各种状态转移逻辑分布到State的子类之间,来减少相互间的依赖,好比把整个版面改成了一个又一个的活字,此时就容易维护和扩展了。
什么时候应该考虑使用状态模式呢?
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。另外如果业务需求某项业务有多个状态,通常都是一些枚举常量,状态的变化都是依靠大量的多分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,某一天客户需要更改需求,增加或减少业务状态或改变状态流程,对你来说都是不困难的事。
工作状态-状态模式版
代码结构图
抽象状态类,定义一个抽象方法“写程序”
1 | // 抽象状态 |
上午和中午工作状态类
1 | // 上午工作状态 |
下午和傍晚工作状态类
1 | //下午工作状态 |
睡眠状态和下班休息状态类
1 | // 睡眠状态 |
工作类,此时没有了过长的分支判断语句。
1 | // 工作 |
客户端代码,没有任何改动。但我们的程序却更加灵活易变了。
1 | class Program |
结果表现如下
1 | 当前时间:9点 上午工作,精神百倍 |
此时的代码,如果要完成‘员工必须在20点之前离开公司’,只需要增加一个‘强制下班状态’,并改动一下‘傍晚工作状态’类的判断就可以了。而这是不影响其他状态的代码的。