状态模式是面向对象多态性原则的极好体现,同样的方法根据实际状态可以实现不同的操作,但我们不能滥用状态模式。
问题提出
假设我们要创建一个类似Photoshop的画图应用程序。
选择某个工具,比如选择画笔工具,按下并拖动鼠标,在画布上画画,释放鼠标就可以画出线条;如果选择橡皮擦工具,则会清除绘制的内容;如果是选择工具,则可能会框出一个虚线的矩形。总之要根据不同的工具,在画布上渲染不同的图形。画布的事件是按下鼠标(mouseDown)和释放鼠标(mouseUp)。
用代码来模拟实现下。
创建一个枚举enum,名为ToolType,枚举值为SELECTION、BRUSH、ERASER分别表示选择工具、画笔工具、橡皮擦工具。
再创建画布类Canvas,该类有2个方法mouseDown和mouseUp,1个属性currentTool代表选择的工具。在mouseDown和mouseUp方法里,分别判断选择的工具,然后画出相应的形状。
上面的if判断语句也可使用switch语句,但不管如何,判断的代码太长了。
实际项目中,除了鼠标事件,还有很多键盘事件;除了brush、eraser、selection工具,还有更多的工具,我们需要对每一种事件,每一个工具都要进行判断,这将是一串长长的判断代码,代码变得难以维护,也缺乏可扩展性。
我们应该使用面向对象的哪个原则来解决呢?
解决方案
显然,我们要利用面向对象的多态性原则。
在本系列第一篇介绍多态性原则时,我们举过UIControl的例子。
UIControl是一个抽象类,约定了draw抽象方法,它不能生成实例,而是由它的子类实现draw方法后生成实例,比如TextBox和CheckBox。在Main类中,只依赖于UIControl,它根据不同的实例,画出不同的图形。这样我们就可以摆脱众多丑陋的if语句。
回到我们的画布Canvas,它与UIControl很类似。
Canvas由Tool组成,Tool定义了mouseDown和mouseUp方法,但它不生成实例,而是由实现了mouseDown和mouseUp方法的子类比如Selection、Brush生成实例。Canvas根据实际的实例画出不同的图形。
这个UML图就是状态模式(State Pattern)。
“四人帮”定义的状态模式命名是不同的,前面我们就说过,没必要按“四人帮”定义的抽象名字命名,而更应该使用有意义的名字。
我们看到,官方的类名分别是Context、State、ConcreteStateA、ConcreteStateB…,mouseDown和mouseUp也用了一个通用的名字handle。
代码实现
首先,我们创建抽象类abstract class Tool,有两个抽象方法mouseDown和MouseUp,由于该抽象类没有任何逻辑,只有约定,使用接口interface更简洁。接着创建SelectionTool和BrushTool,它们均是interface Tool的实现。
完成了工具Tool类,再到画布Canvas类将其组合。Canvas类的mouseDown和mouseUp方法非常简单,就是委托currentTool的mouseDown和mouseUp方法。
最后到Main类,生成Canvas实例,设置currentTool,然后调用mouseDown或mouseUp方法,就会根据实际的currentTool实例调用相应的mouseDown和mouseUp方法。从而避免了一长串判断的if 语句,非常漂亮。
通过状态模式的改造,Canvas可维护性大大提高了,而且扩展也非常容易,比如我们要增加一个橡皮擦工具EraserTool,只需添加EraseTool类并实现Tool接口即可。
状态模式非常符合软件设计中的“开放闭合原则”。我们的类应该对扩展开放对修改闭合。我们不允许修改类现有的代码,我们只能扩展它。扩展功能而无需修改已有的代码。
为什么要这样?
我们可能都碰到过这种情况,当我们修改了一些代码,会发现原有的功能不正常了。开放闭合原则就是为了阻止这种情形,这样我们只需测试扩展的功能,而不用担心影响现有功能,从而使我们的应用程序扩展性更好,也更加健壮。
滥用设计模式
设计模式容易被人滥用。我们得知道,每一种设计模式都是要解决一个特定的问题的。盲目使用设计模式,只会得到越来越多的组件增加复杂性,不但不会解决问题,而且会创建新问题。
软件开发者首先得理解问题,再思考各种可能的解决方案 ,然后选出最适合的。过度设计是那些代码猴子所为,他们只盲目写代码而不理解要解决的问题。
正如列奥纳多·达芬奇所说,简单就是终极的复杂。
滥用状态模式
比如我们要实现一个秒表应用程序,该应用程序有一个带click方法的Stopwatch类,当我们按压(click)时,秒表要么开始要么停止。
我们用if语句判断实现了该应用。很简洁。
有些代码猴子可能会觉得,stopped和running是不同的状态,操作都是click,应该用状态模式实现。
我们将这个应用改造成状态模式,看看是什么样子。
首先要创建定义了click()方法的接口State,再创建实现了State的StoppedState类和RunningState类,这2个类在click时要设置Stopwatch对象的状态,所以要设置一个stopwatch属性并在调用click时改变状态。
回到Stopwatch类,它持有currentState属性,初始设成StoppedState,在click时,委托currentState调用实际的click方法。
最后在Main类中测试效果。
通过重构,我们发现,使用状态模式实现秒表功能要复杂得多,而原有的if判断则非常简洁,显然,这里是不合适使用设计模式的。原因是,秒表就是运行和停止两种状态,不会有其他状态,简单的判断足够了。
所以,我们首先要真正理解问题,再来决定是否使用某个设计模式。
记住,不用滥用设计模式!
小结
本文介绍了状态模式,它是面向对象多态性原则的体现,同样的方法根据实际状态可以实现不同的操作。