最近學習了設計模式的State模式,因為曾碰到過HTML的解析問題,正好需要使用狀態機來分析,所以就嘗試了使用State模式來寫HTML的解析過程,雖然這有點殺雞用牛刀的味道,但對設計模式的理解也有不少的收獲,在此和大家分享一下。 一、HTML解析分析 HTML解析基本上是要對每個字符進行判斷,字符內容大概可以分以下幾類: : < : > : / : = : 字母 : 數字 : 下劃線 : 漢字 另外還有用於注釋的<!--、-->,這裡暫不處理。 對於HTML的解析可以用狀態圖來分析如下: 圖1) 從上圖可以看出,HTML的狀態基本上是處理標簽名、標簽值和標簽屬性這三大類狀態,如果再細分的話,標簽名還可以再分為左標簽名和右標簽名,屬性還可以再分為屬性名和屬性值,當然還有一些情況,如注釋和一些錯誤的數據,這些細節就暫不考慮。 如果這裡單單用一個switch的分支處理的話,代碼就會很復雜,起碼要用兩層switch,第一層用於判斷當前讀取的字符,第二層需要判斷當前的狀態。偽代碼如下:
- do {
- char ch = *itChar;
- switch(ch)
- {
- case '<':
- switch(State)
- {
- case 開始狀態:
- 開始處理…
- 切換到左標簽狀態
- break;
- case 標簽值狀態:
- 標簽值狀態處理…
- break;
- }
- break;
- case '>':
- switch(State)
- {
- case 左標簽名狀態:
- 左標簽名狀態處理…
- 切換到標簽名狀態
- break;
- case 右標簽名狀態:
- 右標簽名狀態處理…
- break;
- }
- break;
- …
- }
- } while(字符串讀取未結束)
第二層的每個分支都可以寫成一個函數,如果有狀態增加的話,那麼就要增加case的分支處理。 是否有更好的方法可以不用switch語句呢?當然你會說,可以使用循環處理,照個思路的話,那就要為循環處理建立一張映射表,用於進行狀態的切換。如果不用映射表呢,有什麼好的辦法麼。 二、state模式 設計模式中的State模式就可以很好的解決這個問題,當然這裡面就要用到很多的類。我們先看下面的關系圖
圖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的一個應用來描述。設計模式裡的各個模式都不是孤立的,需要針對具體情況搭配的使用,在一個代碼裡可能會用到若干種模式,善用這些對提高代碼的質量和可復用性很有好處。您如果有什麼更好的建議和想法,請不吝賜教,或對我的描述有什麼疑問或錯誤的,也請慷慨指出。
本文出自 “飄~~~” 博客,請務必保留此出處http://xulin.blog.51cto.com/264387/410565