当前位置:C++技术网 > 资讯 > e学编程之《设计模式》篇-状态模式

e学编程之《设计模式》篇-状态模式

更新时间:2015-07-27 16:17:15浏览次数:1+次

本文为大家讲解设计模式之状态模式。  

一、 为什么要用状态模式?相信不少朋友在学习设计模式时,都会感觉很困难,看着经典的教材,却味同嚼蜡,里面的术语、各种抽象概念让你感到迷茫,举步维艰,以至于放弃设计模式的学习。e学编程之《设计模式》篇为系列教程,根据笔者的学习、研究和开发经验撰写而成,旨在以通俗、易懂、生动的语言来讲解设计模式,并在讲解的过程中贯穿大量的实例,让初学设计模式的朋友拨云见日,从此觉得设计模式不再抽象,不再难学!笔者假设你已经具备了面向对象编程的基础知识,除些之外,对于理解本文其它的额外知识都不是必需的。鉴于笔者的水平有限,讲解的过程上可能会有一些错误和不足,也希望各位高手和老师批评指正,共同进步。

在一个软件中,某些对象往往不止有一种状态,例如在腾讯QQ中,一个用户有在线、隐身、忙碌等状态;在一个画图工具中,画笔可能有正方形、圆形和三角形等状态。

当这些对象处于不同的状态时,同一个操作有着不同的行为。以上述的的画图工具为例,当画笔处于正方形状态时,执行“画元素”这个操作,系统将会在画板上画一个正方形;而当画笔处于圆形状态时,执行“画元素”这个操作,系统所做的行为是在画板上画一个圆形。

当我们不使用状态模式时,如何处理上述这种情况呢?最简单的方法就是用多分支的条件语句,判断当前的画笔是正方形、圆形还是三角形状态,为每种情况编写不同的“画元素”函数。但是这样做并不“好”,体现在下面几个方面:

1) 使得系统的可扩展性和可维护性大大下降。当我们需要为我们系统中的某个元素增加新的状态时,我们要修改若干我们已有的代码,这样就使得系统的可扩展性和可维护性大大下降。继续以上述的画图工具为例,现在又需要增加菱形这种状态,那么此时我们就要在我们现有的代码中增加一个新的用数值定义的内部状态,然后在我们现有的条件分支中增加一个新的分支和操作。

2)使得代码中遍布看起来很相似的条件语句或case语句,代码非常庸肿冗余。上述的画图工具中画笔有三种状态,条件语句至少有三个分支或者三个case

3) 其状态仅表现为对一些变量的赋值,这不够明确。如果用013三个整型变量分别表示正方形、圆形和三角形三种状态,当我们当前的状态是正方形时,就给表示状态的数值型变量赋值为0,圆形和三角形类似。这样状态的表现不明确,代码的可读性和可理解性大大下降,容易引入各类错误和问题。

那们我们如何写出重用性、可读性、可理解性、可拓展性和可维护性更强的代码呢?这时我们就有必要使用状态模式来重构我们的代码。

二、什么是状态模式?

状态模式(State Pattern)允许一个对象在其内容状态改变时改变它的行为,对象看起来似乎修改了它的类[1]。
   笔者讲到上面这句话时,你可能已经感到很迷茫。别着急,这句话是从经典教材是摘录下来的,难理解是理所当然的。让我们继续以上述的画图工具为例来解释这句话。

首先解释“允许一个对象在其内容状态改变时改变它的行为”这句话。假设当前的画笔为正方形状态(这个画笔即是上面这句话中提到的“对象”),当用户执行“画元素”这个操作,此时系统的行为是在画板上为我们画一个正方形。而当我们将画笔切换为圆形状态时(即上面这句话中提到的“在其内容状态改变时”),此时系统的行为是画圆形。同样的操作(“画元素”),不同的行为(前者为画正方形,后者为画圆形)。这就是所谓的“允许一个对象在其内容状态改变时改变它的行为”。

那么“对象看起来似乎修改了它的类”这句话该如何理解呢?假设我们画笔类为CPen,我们定义该类的一个对象oPen。当该对象处于不同状态时,执行“画元素”这个操作(即调用该对象“画元素”成员函数:oPen.DrawElement())的行为是不同的。而CPen这个类和它的成员函数DrawElement()并没有任何变化,所以我们说“对象看起来似乎修改了它的类”。这句话包含两个意思,一是类操作的行为确实改变了,二是我们使用的是同一个类,调用的是该类的同一个方法,我们并没有修改该类。

这时你可能会迫不及待得问:“咦,这到底是怎么实现的呢?听起来好神奇椰!!!”别着急,下面我们就详细的讲解到底该如何实现状态模式。

三、如何实现状态模式?

我们以一个有趣的小程序为例来详细介绍如何实现一个状态模式(以C++为例)。
   青蛙王子有两种状态,一种是帅气的王子状态,另一种是丑陋的青蛙状态,而美丽的公主的一个吻就可以将青蛙王子从青蛙状态转换为王子状态。当青蛙王子处于青蛙状态时,其“说话”操作的行为是发出“呱呱呱”,处于王子状态时,其“说话”操作的行为是发出“美丽的公主,我爱你!”,如图1所示。即我们上面讲到的,一个对象
(青蛙王子)在其内容状态改变(从青蛙到王子)改变它的行为(“说话”操作从“呱呱呱”到“我爱你!美丽的公主!”)[1]。

图片 
1 青蛙王子示意图 

笔者希望你目前具有UML的基础知识,但是下面的讲解对于完全没有接触过UML的朋友也不会有任何困难。状态模式的静态结构用UML语言描述如图2所示(该UML图与参考文献[1]略有不同,参考文献[1]中的图CSate类的操作命名为Handle(),而CContext类的操作命令为Request()。但是状态模式的核心思想其实就是将对象的操作委派给状态类对象的操作来实现,所以Request()操作其实就是Handle()操作。笔者认为将这两个函数的名字命名为一样的,更能体现状态模式的核心思想,也使得代码的可读性更高。[2]

图片 
状态模式UML表示 

看到这个图,也许你又会感到非常迷茫,而且可能会觉得这个抽象的图形和上面讲到的美丽童话没有任何关系。别着急,让我们一步一步来看这个图。
    1)
首先,实心菱形表示类CContext和类CState是组合关系,即类CContext包含一个类CState的对象。为什么要这样做呢?可以这样理解,类CContext存在多种状态,用类CState来标识。用青蛙王子的例子来说,青蛙王子有青蛙状态和王子状态,用C++代码来实现,即是CFrogPrince类(青蛙王子类),里面包含一个类CState(状态类)的对象oState,用来标识青蛙王子当前所处的状态。
    2)其次,Context类有一个成员函数Opertaion(),我们并未在这个成员函数中直接实现我们的算法,而是在这个函数中调用类State的成员函数Operation()。也就是说,状态模式将对象的操作委派给状态对象来实现。 用青蛙王子的例子来说,我们需要在CFrogPrince类中定义一个Say()成员函数,然后在Say()成员函数中调用类State的Say()成员函数 
    2)再次,空心三角开表示类State和类ConcreteStateA和类ConcreteStateB是继承关系。为什么要这样做呢?我们上面讲到,状态模式主要处理一个元素有多种状态的情况,那么怎么表示多种状态呢?显然我们就要用多个具体的状态子类继承一个状态父尖来表示多种状态。用青蛙王子的例子来说,青蛙王子有青蛙状态和王子状态,用C++代码来实现,即是定义FrogState类(青蛙状态类)和PrinceState类(王子状态类),这两个类均继承父类State
    3)最次,你可能已经注意到了,我们的每个状态子类都实现了一个Say()成员函数,这样我们就可以实现不同的行为。用青蛙王子的例子来说,我们需要实现FragState类和PrinceState类各自的“说话”操作行为
    如果说上面的讲解仍然让你觉得迷茫,那我们下面就根据上面的讲解来实现具体的代码。

四、状态模式的代码实现

首先,我们要定义一个Cstate类(状态类)。代码如下:

class Cstate

{

public:

virtual void Say() = 0;

};
     其次,又因为青蛙王子有青蛙状态和王子状态,所以我们再分别定义两个子类CFrogState(青蛙状态类)和CPrinceState(王子状态类),均继承父类 Cstate,并分别实现各自的成员函数Say(),以实现状态改变时行为也改变。代码如下:
    
class CFrogState : public Cstate

{

public:

void Say()

{

std::cout << "呱呱呱!!!" << std::endl;

}

};

class CPrinceState : public Cstate

{

public:

void Say()

{

std::cout << "美丽的公主,我爱你!!!" << std::endl;

}

}; 
    当然,我们还要定义我们这个程序的主角-CFrogPrince类(青蛙王子类)。 我们要在
CFrogPrince类中定义CState类的对象oState,以表示:青蛙王子有多种状态显然,我们还要定义青蛙王子的操作Say(),但是Say()函数中并没有自己的算法,而是调用了oState(状态类对象)的Say()成员函数,即我们上面所说的:状态模式将对象的操作委派给状态对象CFrogPrince类中还要定义一个Kiss()函数(公主的吻),用于状态的改变(青蛙王子被公主深情一吻后,其状态将从青蛙变为王子)。代码如下:

class CFrogPrince

{

private:

Cstate* oState;

public:

CFrogPrince() : oState(new CFrogState())

{

}

void Say()

{

oState->Say();

}

void Kiss()

{

delete oState;

oState = new CPrinceState;

}

};
    最后呢, 我们要编写我们程序的主函数。代码如下:

int main()

{

CFrogPrince oFrogPrince;                // 青蛙王子登场

oFrogPrince.Say();                      // 当前青蛙王子处于青蛙状态,其“说话”操作发出“呱呱呱!!!”

oFrogPrince.Kiss();                     // 受到公主的深情一吻,青蛙王子从青蛙状态改变为王子状态

oFrogPrince.Say();                      // 此时青蛙王子处于王子状态,其“说话”操作发出“美丽的公主,我爱你!!!”

return 0;

}
    运行程序,输出结果如图3所示。 

 图片 
3 输出结果 
    结果不需要笔者再多解释了吧,同一个对象,状态改变时,其行为也改变。上面已经重复很多次了,这里就不再赘述了。终于学会了状态模式,是不是感觉很激动呢?赶快去自己写代码试试吧!
附:青蛙王子程序完整源码
  
#include <iostream>

  class Cstate
  {
  public:
virtual void Say() = 0;
  };

  class CFrogState : public Cstate
  {
  public:
void Say()
{
std::cout << "呱呱呱!!!" << std::endl;
}
  };

  class CPrinceState : public Cstate
  {
  public:
void Say()
{
std::cout << "美丽的公主,我爱你!!!" << std::endl;
}
  };

  class CFrogPrince
  {
  private:
Cstate* oState;
  public:
CFrogPrince() : oState(new CFrogState())
{
}
void Say()
{
oState->Say();
}
void Kiss()
{
delete oState;
oState = new CPrinceState;
}
  };

  int main()
  {
CFrogPrince oFrogPrince;                // 青蛙王子登场
oFrogPrince.Say();                      // 当前青蛙王子处于青蛙状态,其“说话”操作发出“呱呱呱!!!”
oFrogPrince.Kiss();                     // 受到公主的深情一吻,青蛙王子从青蛙状态改变为王子状态
oFrogPrince.Say();                      // 此时青蛙王子处于王子状态,其“说话”操作发出“美丽的公主,我爱你!!!”
return 0;
  }
参 考 文 献
   [1] Bruce EckelThinking In C++Upper Saddle RiverPrentice Hall2000
   [2] Erich GammaRichard HelmRalph JohnsonJohn VlissidesDesign Pattern: Elements of Reusable Object-Oriented Software[M]BostonAddison-Wesley1995