最近学习了设计模式的State模式,因为曾碰到过HTML的解析问题,正好需要使用状态机来分析,所以就尝试了使用State模式来写HTML的解析过程,虽然这有点杀鸡用牛刀的味道,但对设计模式的理解也有不少的收获,在此和大家分享一下。
 
一、HTML解析分析
        HTML解析基本上是要对每个字符进行判断,字符内容大概可以分以下几类:
  : <
: >
: /
: =
: 字母
: 数字
: 下划线
: 汉字
  另外还有用于注释的<!--、-->,这里暂不处理。
对于HTML的解析可以用状态图来分析如下:
 设计模式之State学习心得_html
图(1)
    从上图可以看出,HTML的状态基本上是处理标签名、标签值和标签属性这三大类状态,如果再细分的话,标签名还可以再分为左标签名和右标签名,属性还可以再分为属性名和属性值,当然还有一些情况,如注释和一些错误的数据,这些细节就暂不考虑。
如果这里单单用一个switch的分支处理的话,代码就会很复杂,起码要用两层switch,第一层用于判断当前读取的字符,第二层需要判断当前的状态。伪代码如下:
 
  1.        do {  
  2.               char ch = *itChar;  
  3.               switch(ch)  
  4.               {  
  5.               case '<':  
  6.                      switch(State)  
  7. {  
  8. case 开始状态:  
  9.        开始处理…  
  10.        切换到左标签状态  
  11. break;  
  12. case 标签值状态:  
  13.       标签值状态处理…  
  14.       break;  
  15. }  
  16.                      break;  
  17.               case '>':  
  18.                      switch(State)  
  19. {  
  20. case 左标签名状态:  
  21.         左标签名状态处理…  
  22.                             切换到标签名状态  
  23.                             break;  
  24.                      case 右标签名状态:  
  25.                             右标签名状态处理…  
  26.                             break;  
  27. }  
  28. break;  
  29. …  
  30. }  
  31. while(字符串读取未结束)  
 
 
    第二层的每个分支都可以写成一个函数,如果有状态增加的话,那么就要增加case的分支处理。
是否有更好的方法可以不用switch语句呢?当然你会说,可以使用循环处理,照个思路的话,那就要为循环处理建立一张映射表,用于进行状态的切换。如果不用映射表呢,有什么好的办法么。
 
二、state模式
        设计模式中的State模式就可以很好的解决这个问题,当然这里面就要用到很多的类。我们先看下面的关系图

 设计模式之State学习心得_html_02

图(2)
    首先解释下上图,这里起码要建立三个类,Context、State、和ConcreteStateX:
Context是负责执行状态处理的,它包含了一个State*的基类指针,当每次进行状态处理时,就调用Request,而在Request中会通过State*这个基类指针调用ConcreteStateA或ConcreteStateB对象的Handle()。我们可以把一种状态写成一个ConcreteStateX对象,然后把处理放在Handle里面,这样只要循环调用Request()就可以完成所有状态处理。这时,你也许就要问了,状态和状态直接的切换如何实现呢?关键就在这里,因为在Context里有个State*,我们只需要更改State*所指的对象就可以了。那么什么时候更改这个指针呢,我们可以把这个处理放在Handle里面,当我们需要切换状态时,调用State中的ChangeState(…, state : State *)方法,通过参数将State对象传入(参见代码),然后更改Context中的State*指针,这里需要注意的是,在Context里面需要将State声明为firend,这样做的好处就是可以直接访问Context中的私有字段,而将其他的类拒之门外。我们在Context里面还加了个方法ChangeState,当然也可以直接赋值,不调用这个方法,但这样写有个好处,我们待会再讲。
        讲到这里,你也许会问了,一个状态就要用一个类,那么状态多了就要创建很多个类对象,这么多个类对象如何管理呢,一个比较简单的方法就是将每个状态类用单例实现,这样就不需要关心对象的创建和释放。还有一种方法是使用工厂模式,将这些类集中创建,集中释放。但是你是否考虑到释放的环节呢,在什么地方释放是最合适的。对了,就在我们刚才提到的ChangeState中,当我们重新赋值state时,就可以把前面的一个State对象给delete掉,如果是单例,就调用Release()方法,这样对需要使用大量内存的状态对象时是很有好处的。
       如果你明白以上的内容,那么下面的代码你就很容易能看明白了:
//state.h
#ifndef _STATE_H_
#define _STATE_H_
class Context; //前置声明
class State
{
public:
       State();
       virtual ~State();
       virtual void Handle(Context* ) = 0;
protected:
       bool ChangeState(Context* con,State* st);
};
class ConcreteStateA:public State
{
public:
       ConcreteStateA() {};
       virtual ~ConcreteStateA() {};
       virtual void Handle(Context* );
};
class ConcreteStateB:public State
{
public:
       ConcreteStateB() {};
       virtual ~ConcreteStateB() {};
       virtual void Handle(Context* );
};
#endif //~_STATE_H_
 
 
//state.cpp
#include "Context.h"
#include <iostream>
using namespace std;
void State::Handle(Context* con)
{
       cout<<"State::.."<<endl;
}
bool State::ChangeState(Context* con,State* st)
{
       con->ChangeState(st);
       return true;
}
void ConcreteStateA::Handle(Context* con)
{
       cout<<"ConcreteStateA::OperationInterface......"<<endl;
       this->ChangeState(con,new ConcreteStateB()); 
}
void ConcreteStateB::Handle(Context* con)
{
       cout<<"ConcreteStateB::OperationInterface......"<<endl;
       this->ChangeState(con,new ConcreteStateA()); 
}
 
//context.h
#ifndef _CONTEXT_H_
#define _CONTEXT_H_
class State;
class Context
{
public:
       Context();
       Context(State* state);
       ~Context();
       void Request();
protected:
private:
       friend class State; //表明在State类中可以访问Context类的private字段
       bool ChangeState(State* state);
private:
       State* _state;
};
#endif //~_CONTEXT_H_
 
//context.cpp
#include "stdafx.h"
#include "Context.h"
#include "State.h"
Context::Context()
{
}
Context::Context(State* state)
{
       this->_state = state;
}
Context::~Context()
{
       delete _state;
}
void Context::Request()
{
       _state->Handle(this);
}
bool Context::ChangeState(State* state)
{
       if (_state) delete _state;
       _state = state;
       return true;
}
 
//测试代码
int StateTest()
{
       State* st = new ConcreteStateA();
       Context* con = new Context(st);
       con->Request();
       con->Request();
       con->Request();
       if (con != NULL)
              delete con;
       if (st != NULL)
              st = NULL;
       return 0;
}
 
       故事讲到这里,还没有结束呢,从图(1)我们可以看到,每个状态常会带个Entry处理和Exit处理,在进入状态和离开状态时,我们经常会需要进行一些处理,因为每个状态会有重复状态的处理,我们把重复性的处理单独分离出来。具体实现的时候,有三种方案:
一、我们可以在调用Request之前调用Entry方法(),并且加上一个状态判断,检查是否是同一个状态,这样就需要加个额外的变量记录当前的状态。Exit()方法则可以加在State::ChangeState()的方法中在调用Context::ChangState()之前调用,这样就可以了。
二、将每个状态类用单例实现,在类的构造中调用Entry(),在析构中调用Exit(),这样在ChangeState中调用对象的Release()即可调用Exit(),Entry可以在对象的Instance()时被调用,这种方法遗留了一个缺陷,下个状态的Entry会在上个状态的Exit()前被调用。
三、可以通过使用工厂模式,在类的构造中调用Entry(),在析构中调用Exit(),当然这就需要在ChangeState中调用delete和new了,既然使用了工厂模式,那么在调用ChangeState的传入的参数就不需要使用对象了,而是可以是状态ID,在Constext::ChangeState里,先delete掉前一个状态对象,然后再调用new创建新的对象,这样代码就比较完美了。
    State模式的应用心得就讲到这里了,HTML的代码如何实现就不是这么简单了,而解析过程只是为了显示HTML服务,这只是实现的一部分,而真正要实现HTML的浏览难就更复杂了,使用栈处理可能会高效一些,这里我们只是将其作为State的一个应用来描述。设计模式里的各个模式都不是孤立的,需要针对具体情况搭配的使用,在一个代码里可能会用到若干种模式,善用这些对提高代码的质量和可复用性很有好处。您如果有什么更好的建议和想法,请不吝赐教,或对我的描述有什么疑问或错误的,也请慷慨指出。