[心得] Simple Factory in Message-Based C/S …

看板C_and_CPP作者 (小乖)時間15年前 (2010/06/18 09:00), 編輯推噓12(1200)
留言12則, 12人參與, 最新討論串1/1
最近在 porting 一個 Client / Server 架構的網路程式, Client 利用 Message 的方式跟 Server 溝通。 例如: client 要向 server 要版本資訊,就傳一個 message struct 給 server struct msg{ int msg_number; int size; void* buf; }; #define GET_VERSION 100 #define GET_USER_INFO 101 // client struct msg m; m.msg_number = 100; // 100 代表 GET_VERSION ... send(m); // 透過網路傳輸 recv(buffer); // 得到 server 的回傳結果 其中 client / server 溝通的協定是透過 msg_number, 上面 msg_number 100 代表是 GET_VERSION (得到版本資訊) 若是要求 server 傳回使用者資料,則 msg_number 就設成 101 以下是 server 的處理 while(1){ // 取得 msg ... if(msg == 100){ // 將版本資訊放入 buf 並回傳 (透過網路) ... }else if(msg == 101){ // 將使用者資訊放入 buf 並回傳 ... } .... else{ // not implement } } 典型的 C style 寫法,很容易理解, (重點不在網路溝通,我就沒有把網路溝通的 code 寫進去了), 但我發現 msg 大概有一百多種 (101~255),server 的處理會很恐怖 ,整個 while (1) 迴圈大概快兩千行 code .... ( ̄□ ̄c|||)a 剛好最近讀了很多 OO 的書,腦袋裝滿了 Design Patterns, 就忍不住來Refactor (重構) 一下 XD 歸納 server code 寫不好的原因如下: 1. 王大娘的裹腳布,又臭又長 (一個函式兩千行,就算 Source Insight也為難) 2. 若要增加新的 msg , 需要動到主程式 ,增加風險 (若不小心 crtl+c , ctrl + v 到別的 msg 就舒服了 ~~) 3. server 主程式跟 msg 間的耦合度太高 (若需資加版本資訊的 log 紀錄,則還是要動到 server 主程式) 以物件導向的術語來說,這也就是違反了 "開放-封閉原則" (OCP:The Open-Closed Principle), 對修改封閉,對擴充開放。 目前的寫法來說,無論是修改或是擴充都要動到 server 主程式,最好的方式是主程式不動, 若要擴充 msg 則新增一個檔案去實作及可。 我這裡用 C 和 C++ 的方式來講此 server code 作重構 ===================== Refactoring in C's style ===================== // 將處理 msg 的 code 包成函式 GET_VERSION ==> int ProcessGetVersion(void* buf); GET_USER_INFO ==> int ProcessGerUserInfo(void* buf); ... // 宣告函式指標陣列 int (*func_ptr [OP_COUNT+1])(void *buf) = {//OP_COUNT 表示 msg的個數 ProcessGetVersion, ProcessGetUserInfo, ... } // server code while(1){ // get Msg ... int ret = (*func_ptr[message_code-100])(m.buf); } 這樣寫後 server 主程式就很乾淨了,要擴充 msg server code 的部分一行都不用 動,只需要寫一個 process 函式,在註冊到函式指標陣列 fun_ptr 即可。 註: 函式指標的用法可以參考 << The C Programming Language >> ===================== Refactoring in C++'s style ===================== 物件導向還有一個原則,針對介面寫程式,不要針對實作寫程式,上面 C style 的方法中,每個處理函式都是實作,這樣當 msg 需要做些修改時 (例如為每次的處理加入 log 機制) 都要修改其實作。 觀察一下 server 的行為 (1) get msg (2) process msg 。既然每個 msg 都要做處理, 那就把要做處理的動作抽象起來 (也就是提升為父類別),每個子類別再覆寫其處 理行為。 class 類別階層如下: // abstract class class ProcessMsg { virtual int process(void* buf) = 0; virtual ~ProcessMsg(); }; class ProcessGetVersion:public ProcessMsg { ProcessGetVersion:public (); virtual int process(void* buf); }; class ProcessGetUserInfo: public ProcessMsg { ProcessGetUserInfo(); virtual int process(void* buf); }; 接下來再用簡單工廠方法 Simple Factory Method 把產生物件的實作作封裝。 如下: // server code while(1){ // get Msg ... ProcessMsg* obj = simpleFactory(message_code); int ret = obj->process(); } ProcessMsg* simpleFactory(int message_code) { if( message_code==GET_VERSION){ return new ProcessGetVersion(); }else if(message_code==GET_USERINFO){ return new ProcessGetUserInfo(); ... }else{ } } 這樣一來就達到"針對介面寫程式,不要針對實作寫程式"的 OO 守則了 (從 server code 的 ProcessMsg* obj = simpleFactory(message_code); 此行觀之)。 這樣寫的語意為, server 主程式得到了一個 ProcessMsg 物件,他不知道實際上 的處理方式 (是要 get_version 還是 get_userInfo),只知道此 ProcessMsg 都需要被 process 因此就直接呼叫 obj->process() , 讓多型機制來決定到底是要 get_version 還 是 get_userInfo。 code 寫到這裡有個很礙眼的地方 = =" 沒錯,就是 simpleFactory() 的內部仍然是用 if else 的方式。若要擴充 msg 仍 然要在 simpleFactory 內部動手腳。 若是可以改成這樣 map<int,string> msg_map; msg_map[100] = "ProcessGetVersion"; msg_map[101] = "ProcessGetUserInfo"; ... ProcessMsg* simpleFactory(int message_code) { return createObject(msg_map[message_code]); } 那 code 就乾淨又漂亮了,要擴充 msg 只需再繼承 ProcessMsg 類別即可。 注意 createObject 生成的參數是字串,也是就是給予類別的名稱字串,就能夠生 成對應的類別,這種在物件導向的術語叫做動態生成 (Dynamic Creation) 例如: ProcessMsg* p = createObject("ProcessGetVersion"); // 比照 new ProcessGetVersion(); ProcessMsg* p = createObject("ProcessGetUserInfo"); 若是在 Java 的話,有反射機制可以達成 (Reflection) C++ 語言本身沒有提供 (嘆..) ,不過 MFC 倒是有這個功能。 不過要繼承 CObject 還有加上兩行 Macro : DECLARE_SERIAL 、DECLARE_SERIAL << 深入淺出 MFC by 侯捷>> 有詳細的說明。 code 如下: (此 code 在 visual 2008 編譯,此為 console mode,在建立 project MFC 選項要打勾) ================================== class ProcessMsg { protected: virtual ~ProcessMsg(void){}; public: virtual int process(void) = 0; }; class ProcessRequestAllLog : public CObject,public ProcessMsg // 這裡的多重繼承對於 ProcessMsg 來說只是介面繼承, // ProcessMsg 語意同 Java 的 Interface { DECLARE_SERIAL(ProcessRequestAllLog) public: ProcessRequestAllLog(){} virtual ~ProcessRequestAllLog(){} virtual int process(void){ cout << "ProcessRequestAllLog" ;} }; IMPLEMENT_SERIAL(ProcessRequestAllLog ,CObject,0x00) ProcessMsg* simpleFactory(string str_msg) // // CRuntimeClass::FromName 的功能好像是 VC7 才開始支援, // VC6++ 應該是不能編譯 XD { ProcessMsg* p = (dynamic_cast<ProcessMsg*> (CRuntimeClass::FromName(str_msg.c_str())->CreateObject())); ASSERT(p!=NULL); return p; } int main() { ProcessMsg* p = simpleFactory("ProcessRequestAllLog"); //這裡展示動態生成的功能,就沒有加入 message_code 的判斷 p->process(); return 0; } ================================== Note: 本以為只要用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 這兩個 Macro 即可。但文件上說要用 DECLARE_SERIAL 、DECLARE_SERIAL http://msdn.microsoft.com/en-us/library/z2z1h62t(VS.80).aspx 這樣的 client / server 架構就比較清爽了~ (希望沒有 overdesign ~ 本來還想要用 Command DP 去做...) 跟大家分享一下~ -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 61.216.174.15

06/18 17:15, , 1F
thank for sharing...:D
06/18 17:15, 1F

06/18 17:38, , 2F
good
06/18 17:38, 2F

06/18 18:09, , 3F
Good
06/18 18:09, 3F

06/18 18:19, , 4F
nice~
06/18 18:19, 4F

06/18 19:30, , 5F
Push.
06/18 19:30, 5F

06/18 20:14, , 6F
push~~
06/18 20:14, 6F

06/18 21:21, , 7F
good job
06/18 21:21, 7F

06/18 21:32, , 8F
06/18 21:32, 8F

06/18 22:27, , 9F
<(_ _)>
06/18 22:27, 9F

06/19 00:57, , 10F
push la~
06/19 00:57, 10F

06/19 04:25, , 11F
沒有提供就自己寫一個吧~ http://ppt.cc/jj96
06/19 04:25, 11F

06/22 01:22, , 12F
push!
06/22 01:22, 12F
文章代碼(AID): #1C6pMN3u (C_and_CPP)