現在我們看看一個實際的例子,從不同設計原則的角度來觀察它的設計。這個例子是資訊處理中心-資料轉送控制組件的訊息處理模組,首先看看它的結構圖、類實現虛擬碼和主體程式虛擬碼:
圖3-1 訊息處理模組結構圖
程式3-1 類實現虛擬碼
///////////////////////////////////////////////////////////////////////////////////
Command 類
///////////////////////////////////////////////////////////////////////////////////
/* 多線程的痛點在於線程的管理和線程的同步
以下的虛擬碼很好地完成了這兩方面:
1: 用Windows線程池實現線程管理
2: 用CCommand對象實現線程同步
*/
class CCommand
{
public:
CCommand()
{
::InterlockedIncrement(&sm_ExecutingCommandCount);
}
virtual ~CCommand()
{
::InterlockedDecrement(&sm_ExecutingCommandCount);
}
virtual int Execute() = 0;
void SetContent(const String& content)
{
m_sContent = content;
}
// 正在處理的命令數量
volatile static int sm_ExecutingCommandCount = 0;
// CSWMRG類實現“多個線程同時讀,但不能同時讀寫或同時寫”的功能
static CSWMRG sm_SWMRG;
protected:
// 處理訊息的個CCommand子類只需重載該方法
virtual int Process() = 0;
private:
String m_sContent;
};
/*
子類只需要重載Process()來處理各種請求(解析m_sContent、處理m_sContent)就OK!!!
可以當作單線程程式一樣放心操作
不必擔心緩衝的同步和對象的釋放等問題
*/
// SCP命令繼承該類
class CReadCommand : public CCommand
{
virtual int Execute()
{
sm_SWMRG.WaitToRead();
int retval = Process();
sm_SWMRG.Done();
return retval;
}
};
// 更新命令繼承該類
class CWriteCommand : public CCommand
{
virtual int Execute()
{
sm_SWMRG.WaitToWrite();
int retval = Process();
sm_SWMRG.Done();
return retval;
}
};
程式3-1 主體程式實現虛擬碼
///////////////////////////////////////////////////////////////////////////////////
主體程式
///////////////////////////////////////////////////////////////////////////////////
void ProcessMsg(const String& header, const String& content)
{
// CCommandFactory根據訊息頭產生相應的CCommand對象
CCommand* pcmd = CCommandFactory::GetCommand(header);
// 設定CCommand對象的內容
pcmd->SetContent(content);
::QueueUserWorkItem(ThreadFunction, pcmd);
}
void ThreadFunction(PVOID pv)
{
CCommand* pcmd = reinterpret_cast<CCommand*>(pv)
int ret = pcmd->Execute();
delete pcmd;
}
程式中包含main、command、scp、notice和cmdfactory四包。其中,main包包含應用程式的高層策論,執行程式的主體代 碼;command包是一個抽象包,它包含CCommand、CReadCommand和CWriteCommand三個抽象類別;scp包包含了所有處理 SCP協議的命令對象;notice包包含了所有處理緩衝更新通知的命令對象;cmdfactory包包含CCmdFactory類,這個類根據訊息頭生 成CCommand類的相應子類對象。
main包中的程式碼要用到CCommand類和CCommandFactory類,所以它依 賴於command包和cmdfactory;scp包中的類繼承於CReadCommand類所以它依賴於command包;notice包中的類繼承 於CWriteCommand類所以它依賴於command包;cmdfactory包中的CCmdFactory類要用到CCommand抽象類別及其所 有實現子類,所以它依賴於command、scp和notice包。
下面從面各個向對象設計原則的角度來觀察它的設計:
1. 單一職責原則(SRP)
CCommand類及其子類子負責執行命令(Execute方法),而選擇命令的工作由CCommandFactory類負責。試想如果把選擇命令的工 作放在CCommand中會有什麼後果:每增加一個CCommand子類會導致CCommand的命令選擇函數修改,由於CCommand類的修改導致所 有依賴於它的代碼(所有CCommand子類和使用CCommand的客戶代碼)都需要重新編譯,即使它們沒有作過任何改動。
2. 開放-封閉原則(OCP)
當有新的命令要處理時,我們可以新增一個CCommand類的子類用來對這個命令進行處理,另外再對CCommandFactory進行相應修改就可以了。而程式的主體代碼不用修改。
3. Liskov替換原則(LSP)
任何CCommand類的子類替換CCommand基類都使得針對CCommand編寫的主體程式功能不變。
4. 依賴倒置原則(DIP)
主體程式碼(高層模組)不依賴於各個命令的實現子類(低層模組),兩者都依賴於CCommand(抽象)。
CCommand(抽象)不依賴於各個命令的實現子類(細節),各個命令的實現子類(細節)依賴於CCommand(抽象)。
5. 介面隔離原則(ISP)
(本例沒有體現)
6. 重用發布等價原則(REP)
command包的所有類都是可以重用的,其他包的所有類都是不可重用的。
7. 共同重用原則(CRP)
command包是可重用的,但是可能有人會說,command包有小許違反共同重用原則,因為有些代碼可能只用到command包中的 CReadCommand類(如scp包),而另外一些代碼可能只用到command包中的CWriteCommand類(如notice包)。是不是應 該把command包拆分出readcommand包和writecommand包呢?
這是一個設計權衡的問題。拆分出來當然可以,問 題是有沒有必要?會帶來什麼好處?command包中之所以要在CCommand類中派生出CReadCommand類和CWriteCommand類是 因為它除了執行命令外還有另外一個目的――同步讀寫操作。所以它做了這樣的假設:使用command包的程式必定有對讀寫操作進行同步的需求,也就必定會 同時用到CReadCommand和CWriteCommand這兩個類。
那麼scp包只用到CReadCommand類,而 notice包只用到CWriteCommand又怎麼解釋呢?其實這隻是個巧合問題,如果我們增加一個SCP協議包用來進行某些更新操作,它就會用到 CWriteCommand了,同理,如果我們在notice包中增加一個處理唯讀通知的命令,它也會用到CReadCommand。其實我們可以把 scp包和notice包組合成一個大包。這樣就很明顯地看到這個大包同時用到了CReadCommand和CWriteCommand類。對了,為什麼 不把scp包和notice包組合起來而要分開呢?
這是另一個設計問題!當用scp包處理SCP協議時各處理命令之間可能會在其他方面有共同的抽象(如SCP報文資料結構等);同理notice也一樣(如通知的資料格式)。所以scp包和notice包之所以分開是基於其他原因,和這個主題無關。
8. 共同封閉原則(CCP)
把scp包和notice包分開而不是組合在一起還有什麼好處呢?設想當SCP協議格式作了修改,它隻影響所有處理SCP協議的命令類,處理通知訊息的 命令類沒必要因為一個與它無關的修改而跟著一起重新發布;同理,當通知訊息的格式作了修改時,也不應該影響處理SCP協議的命令類。
9. 無環依賴原則(ADP)
從結構圖中可以看到,依賴關係中不存在環。
10. 穩定依賴原則(SDP)
命令選擇規則是非常不穩定的,每當新增一個命令或者商務規則發生改變時它都必須要作出相應修改,所以cmdfactory包應該位於依賴關係的高 層;command包是非常穩定的,無論商務規則變化或者其他的改動都不會影響到它,所以它應該位於依賴關係的低層。也就是說朝著穩定的方向進行依賴。
11. 穩定抽象原則(SAP)
command包是一個非常穩定的包,同時它又是一個抽象包(包含抽象類別);main、scp、notice和cmdfactory包是非常不穩定的,同時它們也是具體的。
是否已經完美?
通過上述的解說,是否說明了這樣的設計已經接近完美了呢?可能是,也可能不是。
大家有沒有注意到,因為main包用到了CCommandFactory類,所以一個依賴關係從main包連到cmdfactory包?而 cmdfactory包又依賴於scp和notice包。這樣就造成了main包中包含高層策略的主體代碼傳遞依賴於scp和notice這兩個包含實現 細節的包;當scp或notice包有改動時會令到cmdfactory包的CCommandFactory類修改,由於CCommandFactory 類的修改從而導致main包的程式主體代碼部分必須重新編譯。
單從main包中的代碼很難發現main包對scp包和notice包的 依賴,在main包的代碼中只看到它依賴於command包和cmdfactory包,看不到它使用了任何一個scp包或notice包中的類。但是在關 系圖中這個依賴關係是一幕瞭然的。嗯……有時圖的表現力的確好於代碼,題外話了~^_^~。
言歸正傳,我們怎樣才能消除上述依賴關係呢?簡單!—— DIP。修改後的結構3-2所示:
圖3-2修改後的訊息處理模組結構圖
這樣,cmdfactory包不再依賴於scp和notice包,無論對scp或notice包作了什麼修改,cmdfactory包和main包都不必重新編譯。
問題是這樣的修改到底有沒必要?如果scp和notice包的改動比較頻繁或者main包的程式主體代碼需要較長的編譯時間,又或者scp和 notice包以共用庫或DLL的形式提供,上述的修改是必要的;否則,當程式的規模較小,程式主體代碼需要的編譯時間可以忽略的情況下,可以不進行修 改。畢竟修改後的程式結構增加了一個抽象層意味著增加了複雜性。
設計原則與設計模式
關於設計模式的介紹請參考[GOF95]中描述的23個設計模式。設計模式的確是個好東西。我們應該怎樣把物件導向設計原則與設計模式結合起來呢?
我通常的做法是先不考慮設計模式,一切簡單至上。畢竟使用了設計模式就或多或少地增加了軟體的複雜性(聞到一些複雜性的臭味沒?)。隨著程式規模的擴大,需要應用物件導向設計原則對軟體中的模組進行抽象和解耦時,再把它迴歸為模式。
CodeProject