為什麼這篇開放封閉原則鄉民發文收入到精華區:因為在開放封閉原則這個討論話題中,有許多相關的文章在討論,這篇最有參考價值!作者spider391 (小乖)看板C_and_CPP標題[心得] Simple Factory i...
開放封閉原則 在 國際內世鏡|Insight Into Issues Instagram 的最佳貼文
2021-09-17 15:29:23
2020東京帕拉林匹克運動會(後簡稱「帕運」)開幕式於今(24)日當地時間晚間8時在國立競技場舉行。然而,在東京COVID-19疫情仍十分嚴峻的情況下,防疫再度成為賽會的重大議題。 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 由於帕運有身障或是患有慢性疾病及重度障礙等基礎疾病選手參賽,若選手染疫,則轉為重症的風險也會相...
最近在 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