代码改变世界

设计模式初学者系列之状态模式

2008-01-18 09:23  横刀天笑  阅读(5765)  评论(11编辑  收藏  举报

设计模式之状态模式

                                        ―――策略模式的孪生兄弟

    在本文中我将通过一个DEMO的迭代来讲述状态模式,这个DEMO以交谈的方式提出,交谈中将有三个角色出现:PM、客户、我。并在最后提一下Martin Fowler的Replace Type Code With State/Stategy重构。然后一起看看状态模式和策略模式的异同。

在我们的开发中会经常碰到这样的情况:根据对象的状态执行相应的操作,比如数据库连接的状态 打开,还是关闭?TCP连接的状态(Gof DP),设备运行的状态,等等。一般我们会怎么做?首先向对象询问当前状态,然后根据状态做出相应的操作。OK,按照这个思路我们就来完成下面这个项目吧。

PM:公司接到一个关于电梯控制的项目。

(说着PM就在纸上画出电梯的控制面板)

这是一个电梯的控制面板,主要就是这个“开门”和“关门”的按钮了,客户要求当电梯运行的时候两个按两个按钮的时候提示“电梯正在运行”,如果门已经是开着的时候点击“开门”按钮将提示“门已经是开着的”,这个时候点击“关门”按钮才关门,开门也是同理。

在PM讲述客户的需求的时候我已经写好了代码:

//这是电梯类,提供了查询状态和设置状态的接口,开门和关门的方法

public class Lift

{

public Lift()

{

_state = LiftState.Running;

}

private LiftState _state;

public LiftState GetState()

{

return _state;

}

public void SetState(LiftState state)

{

_state = state;

}

public void OpenDoor()

{

MessageBox.Show("开门");

_state = LiftState.Open;

}

public void CloseDoor()

{

MessageBox.Show("关门");

_state = LiftState.Closed;

}

}

//电梯的状态

public enum LiftState

{

Open,

Running,

Closed

}

看看控制程序是怎么干的?

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}

Lift lift;

//按关门按钮的时候

private void btnClose_Click(object sender, EventArgs e)

{

if (lift.GetState() == LiftState.Open)

{

lift.CloseDoor();

}

else if (lift.GetState() == LiftState.Closed)

{

MessageBox.Show("当前门是关着的");

}

else

{

MessageBox.Show("电梯正在运行");

}

}

//按开门按钮的时候

private void btnOpen_Click(object sender, EventArgs e)

{

if (lift.GetState() == LiftState.Closed)

{

lift.OpenDoor();

}

else if (lift.GetState() == LiftState.Open)

{

MessageBox.Show("当前门是开着的");

}

else

{

MessageBox.Show("电梯正在运行");

}

}

private void Form1_Load(object sender, EventArgs e)

{

lift = new Lift();

}

//为了模拟出真正的电梯,通过这个按钮将电梯
//从运行状态切换到门关闭的时候状态

private void btnChangeState_Click(object sender, EventArgs e)

{

lift.SetState(LiftState.Closed);

}

}

PM(抬头询问):有什么困难没有?

我:没有,我已经将代码写好了,您看看。(迅速的将写满代码的稿纸递给PM)

PM(看了一会儿)赞许道:效率不错啊,这么一下子就为公司搞定这个单子。OK,为了表示对你的奖励,周末可以不用加班了。

一年过去了,你已经坐上PM的座位了,你当初写下的那个电梯控制程序也一直运行良好,谁说需求总是会变,一年了那个需求就没有变过,但是不愿意看到的事还是发生了

客户打来电话抱怨说:你的程序运行的很好,但是每次关门的时候非要门完全关上后才能按开门,不能在中途开门,关门也是如此。

思考了下,觉得这个变更是合理的,满口答应了客户的要求。心里想着:不就是加两个状态嘛

public enum LiftState

{

Open,

Openning,

Running,

Closing,

Closed

}

并且修改了一下判断条件:

if (lift.GetState() == LiftState.Open || lift.GetState() == LiftState.Closing)

{

lift.CloseDoor();

……………………

if (lift.GetState() == LiftState.Closed || lift.GetState() == LiftState.Openning)

{

lift.OpenDoor();

…………………………………

很快你又交差了,但是这次你有点不爽,当上PM后整天嘴里都是架构、模式,什么开闭原则啊,面向接口编程啊,依赖抽象不依赖具体。可为什么到自己编的时候却总是达不到呢?难道这些OO原则都存在于子虚国?不过心情也不是太差,毕竟完成了客户的需求,还是重复那句古老的经典:如果它还可以运行,就不要动它。

三个月后:

客户:Hello,老兄,我们的电梯接到一个单子,有个电影公司找到我们,要我们提供这样一种电梯,电梯在运行的时候也可以开关门,这样他们就可以拍摄一些特技动作,比如演员跳进正在上升但门是开着的电梯内。

我:…………..

客户:怎么?有困难么?电影公司说要拍一部什么《电梯惊魂》的影片,可能以后有更多的需求变化呢,等电影拍完了有你的好处,呵呵。

我:没有,马上搞定。

我(心里想):我说怎么瞧这设计都不怎么顺眼啊,客户的需求总会变,还这么离谱,倒霉的事情都让我给碰上了,不过那分红clip_image003

随后的几天一直在思考着这个设计,连上厕所心里都是电梯电梯。和往常一样,上厕所随手拿着一本书。这次翻到的是状态模式。

意图:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

嗯?这不正是我要的么?呵呵,踏破铁鞋无觅处,得来全不费工夫。仔细阅读设计后决定重构设计了

先来看看状态模式的类图

还有印象么?和策略模式的类图一模一样。状态模式封装的是对象的一系列状态,将对象的每个状态都用一个独立的子类来表示。当请求到来的时候根据对象当前的状态使用对应的状态子类来完成。

在我们的电梯系统中电梯有六种状态,这样就有六个子类,电梯有“开门”和“关门”两个方法,每个状态都要实现这两个方法。来看看运用状态模式后电梯系统的实现:

//这是状态的父类,所有的状态都必须继承这个类

public class LiftState

{

protected Lift _lift;

public LiftState(Lift lift)

{

_lift = lift;

}

public virtual void OpenDoor() { }

public virtual void CloseDoor() { }

}

//电梯正在运行的状态类

public class LiftRunning : LiftState

{

public LiftRunning(Lift lift)

: base(lift)

{ }

public override void OpenDoor()

{

MessageBox.Show("电梯正在运行");

}

public override void CloseDoor()

{

MessageBox.Show("电梯正在运行");

}

}

//门是关着的状态类

public class LiftClosed : LiftState

{

public LiftClosed(Lift lift)

: base(lift)

{ }

public override void OpenDoor()

{

MessageBox.Show("开门");

_lift.SetState(new LiftOpen(_lift));

}

public override void CloseDoor()

{

MessageBox.Show("当前门是关着的");

}

}

家庭作业:其余的状态类就留给各位同学实现了。

//我们的电梯类,将开门,关门的动作代理到状态类上实现

//当前的状态决定了当前的动作,而动作也可以转变当前的状态

public class Lift

{

LiftState _state;

public Lift()

{

_state = new LiftRunning(this);

}

public LiftState GetState()

{

return _state;

}

public void SetState(LiftState state)

{

_state = state;

}

public void OpenDoor()

{

_state.OpenDoor();

}

public void CloseDoor()

{

_state.CloseDoor();

}

}

再来看看我们的控制界面部分:

//这部分代码是控制界面代码,在实际应用中往往代表着客户代码,这部分代码需要特//别稳固,比如在这个项目中,控制界面部分涉及到硬件的编程,非常复杂,如果我们//能抑制这部分的修改将大大减少bug的发生我们往往需要将变化封装,然后和客户代

//码隔离起来

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}

Lift lift;

private void btnChangeState_Click(object sender, EventArgs e)

{

lift.SetState(new LiftClosed(lift));

}

private void Form1_Load(object sender, EventArgs e)

{

lift = new Lift();

}

private void btnClose_Click(object sender, EventArgs e)

{

lift.CloseDoor();

}

private void btnOpen_Click(object sender, EventArgs e)

{

lift.OpenDoor();

}

}

经过这样的重构后有什么好处?状态模式将与每个状态相关的行为都实现在一个单独的类里面,当需要增加更多的状态的时候只需要增加状态的子类而无须修改客户的代码(对修改关闭,对扩展开放)。电梯将开门/关门的处理代理到状态类,而且面对的是LiftState这个父类(依赖抽象不依赖具体,虽然这里LiftState即不是接口也不是抽象类,但是他起的作用却是是抽象)

和策略模式一样,状态模式也会增加类的数量,但是如果在系统中有非常多的状态而且状态的多少还在变化,这种通过增加子类的做法是有利的,否则你就要写那个又长又臭的面条式条件判断代码。

什么时候使用状态模式

通过前面的阐述,我们基本上了解了状态模式的样子。那我们什么时候使用状态模式呢?来看看Martin Fowler的这个重构:Replace Type Code with State/Stategy 你有一个type code,它会影响class的行为,但你无法使用subclassing。

在你的类里面有个型别码来表示对象的当前状态,这个对象的行为通常依赖这个状态,而且在运行的时候这个状态会改变,那么对象的行为在运行的时候也要跟着改变。一般我们会使用if/else或者switch来根据这个型别码来执行相关操作,现在我们有更好的方式来处理。

状态和策略的异同

有人会说,状态模式和策略模式是如此的相似,何必又分开呢?关键在于状态模式和策略模式的意图,状态模式是封装对象内部的状态的,而策略模式是封装算法族的。而且状态模式往往有这种表现:状态影响着对象当前的行为,行为也会倒过来改变对象的状态,这个相互影响是发生内部,也就是说状态模式中对象的行为是由对象的状态驱动的,而策略模式却不同,每次我们往往只使用一种策略来配置当前的系统,改变策略都是由外力来改变的,要使用哪种算法是由外部对象(客户)来驱动的。

这是我在公司内部讨论时候的手稿,当讨论完后我的经理说了他对状态模式的一个应用:在Gis中的鼠标操作,鼠标有几种操作:点击、右键、拉圈。鼠标有几种模式(状态):放大、缩小、常规 等等,当用鼠标进行操作的时候会将具体的操作代理到鼠标的模式类处理(经理没有提供代码,可以这样猜测)

public class Mouse

{

private MouseState _mouseState;

//提供一个接口,外部可以改变这个状态

public void SetMouseState(MouseState mouseState)

{

_mouseState = mouseState;

}

public void Click()

{

_mouseState.Click();

}

public void DblClick()

{

_mouseState.DblClick();

}

}

public abstract class MouseState

{

public abstract void Click();

public abstract void DblClick();

//...........更多

}

public class LargeState : MouseState

{

public override void Click()

{

//....放大时点击

}

public override void DblClick()

{

//...放大时双击

}

}

public class NarrowState : MouseState

{

public override void Click()

{

//...缩小时点击

}

public override void DblClick()

{

//..缩小时双击

}

}

最后经理还总结了:要使用状态模式首先要总结出我们操作的接口(就是鼠标那些点击,双击操作),保证这个接口稳定,如果这个接口经常变化那么就不建议采用状态模式。

本日志相关源代码下载,里面包括电梯的无状态模式实现和状态模式实现