「C++ 篇」答應我,別再if/else走天下了可以嗎
每日一句英語學習,每天進步一點點:
- "Without purpose, the days would have ended, as such days always end, in disintegration."
- 「少了目標,一天還是會結束,它總是以支離破碎的形式結束。」
前言
羊哥之前寫一篇有趣的文章《答應我,別再if/else走天下了可以嗎 | CodeSheep 》,在文中使用 Java 語言實現了列舉類、工廠模式和策略模式的三種方式,來消除連環的 if / else
。內容層層遞進,由淺入深的方式我非常喜歡。
看到有留言中有小夥伴想看 C++ 版本的,特此寫下了此文(已經過羊哥的同意)。不過由於 C++ 沒有列舉類,所以本文不涉及此方式,但本文會帶大家一步一步的優化工廠模式和策略模式。
正文
糟糕 if / else 連環
if / else
可以說是我們學習程式設計時,第一個學習的分支語句,簡單易理解,生活中也處處有的 if / else
例子:
老婆給當程式設計師的老公打電話:“下班順路買一斤包子帶回來,如果看到賣西瓜的,買一個。”
當晚,程式設計師老公手捧一個包子進了家門。。。
老婆怒道:“你怎麼就買了一個包子?!”
老公答曰:“因為看到了賣西瓜的。”
老婆的思維:
買一斤包子; if( 看到賣西瓜的 ) 買一隻( 西瓜 );
而程式設計師老公的程式:
if( ! 看見賣西瓜的 ) 買一斤包子; else 買一隻( 包子 );
非常生生動動的生活例子!如果身為程式設計師的你,犯了同樣的思維錯誤,別繼續問你媳婦為什麼,問就是跪鍵盤:
進入本文正題。考慮以下栗子:一般來說我們正常的後臺管理系統都有所謂的角色的概念,不同管理員許可權不一樣,能夠行使的操作也不一樣。
- 系統管理員(
ROLE_ROOT_ADMIN
):有A
操作許可權 - 訂單管理員(
ROLE_ORDER_ADMIN
):有B
操作許可權 - 普通使用者(
ROLE_NORMAL
):有C
操作許可權
假設一個使用者進來,我們需要根據不同使用者的角色來判斷其有哪些行為。使用過多 if / else
連環寫法的我們,肯定下意識就覺得,這不簡單嘛,我上演一套連環的寫法:
class JudgeRole { public: std::string Judge( std::string roleName ) { std::string result = ""; if( roleName == "ROLE_ROOT_ADMIN" ) // 系統管理員 { result = roleName + "has A permission"; } else if( roleName == "ROLE_ORDER_ADMIN" ) // 訂單管理員 { result = roleName + "has B permission"; } else if( roleName == "ROLE_NORMAL" ) // 普通使用者 { result = roleName + "has C permission"; } return result; } };
當系統裡有幾十個角色,那豈不是幾十個 if / else
巢狀,這個視覺效果絕對酸爽……這種實現方式非常的不優雅。
別人看了這種程式碼肯定大聲喊:“我X,哪個水貨寫的!”
這時你聽到,千萬不要說:“那我改成 switch / case
”。千萬別說,千萬別說哦,否則可能拎包回家了…
因為 switch / case
和 if / else
毛區別都沒,都是寫費勁、難閱讀、不易擴充套件的程式碼。
接下來簡單講幾種改進方式,別再 if / else 走天下了。
工廠模式 —— 它不香嗎?
不同的角色做不同的事情,很明顯就提供了使用工廠模式的契機,我們只需要將不同情況單獨定義好,並聚合到工廠裡面即可。
首先,定義一個公用介面 RoleOperation
,類裡有一個純虛擬函式 Op
,供派生類(子類)具體實現:
// 基類 class RoleOperation { public: virtual std::string Op() = 0; // 純虛擬函式 virtual ~RoleOperation() {} // 虛解構函式 };
接下來針對不同的角色類,繼承基類,並實現 Op 函式:
// 系統管理員(有 A 操作許可權) class RootAdminRole : public RoleOperation { public: RootAdminRole(const std::string &roleName) : m_RoleName(roleName) {} std::string Op() { return m_RoleName + " has A permission"; } private: std::string m_RoleName; }; // 訂單管理員(有 B 操作許可權) class OrderAdminRole : public RoleOperation { public: OrderAdminRole(const std::string &roleName) : m_RoleName(roleName) {} std::string Op() { return m_RoleName + " has B permission"; } private: std::string m_RoleName; }; // 普通使用者(有 C 操作許可權) class NormalRole : public RoleOperation { public: NormalRole(const std::string &roleName) : m_RoleName(roleName) {} std::string Op() { return m_RoleName + " has C permission"; } private: std::string m_RoleName; };
接下來在寫一個工廠類 RoleFactory
,提供兩個介面:
- 用以註冊角色指標物件到工廠的
RegisterRole
成員函式 - 用以獲取對應角色指標物件的
GetRole
成員函式
// 角色工廠 class RoleFactory { public: // 獲取工廠單例,工廠的例項是唯一的 static RoleFactory& Instance() { static RoleFactory instance; // C++11 以上執行緒安全 return instance; } // 把指標物件註冊到工廠 void RegisterRole(const std::string& name, RoleOperation* registrar) { m_RoleRegistry[name] = registrar; } // 根據名字name,獲取對應的角色指標物件 RoleOperation* GetRole(const std::string& name) { std::map<std::string, RoleOperation*>::iterator it; // 從map找到已經註冊過的角色,並返回角色指標物件 it = m_RoleRegistry.find(name); if (it != m_RoleRegistry.end()) { return it->second; } return nullptr; // 未註冊該角色,則返回空指標 } private: // 禁止外部構造和虛構 RoleFactory() {} ~RoleFactory() {} // 禁止外部拷貝和賦值操作 RoleFactory(const RoleFactory &); const RoleFactory &operator=(const RoleFactory &); // 儲存註冊過的角色,key:角色名稱 , value:角色指標物件 std::map<std::string, RoleOperation *> m_RoleRegistry; };
把所有的角色註冊(聚合)到工廠裡,並封裝成角色初始化函式InitializeRole
:
void InitializeRole() // 初始化角色到工廠 { static bool bInitialized = false; if (bInitialized == false) { // 註冊系統管理員 RoleFactory::Instance().RegisterRole("ROLE_ROOT_ADMIN", new RootAdminRole("ROLE_ROOT_ADMIN")); // 註冊訂單管理員 RoleFactory::Instance().RegisterRole("ROLE_ORDER_ADMIN", new OrderAdminRole("ROLE_ORDER_ADMIN")); // 註冊普通使用者 RoleFactory::Instance().RegisterRole("ROLE_NORMAL", new NormalRole("ROLE_NORMAL")); bInitialized = true; } }
接下來藉助上面這個工廠,業務程式碼呼叫只需要一行程式碼,if / else
被消除的明明白白:
class JudgeRole { public: std::string Judge(const std::string &roleName) { return RoleFactory::Instance().GetRole(roleName)->Op(); } };
需要注意:在使用 Judge
時,要先呼叫初始化所有角色 InitializeRole
函式(可以放在 main
函式開頭等):
int main() { InitializeRole(); // 優先初始化所有角色到工廠 JudgeRole judgeRole; std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; std::cout << judgeRole.Judge("ROLE_ORDER_ADMIN") << std::endl; std::cout << judgeRole.Judge("ROLE_NORMAL") << std::endl; }
通過工廠模式實現的方式,想擴充套件條件也很容易,只需要增加新程式碼,而不需要改動以前的業務程式碼,非常符合「開閉原則」
不知道小夥伴發現了沒有,上面實現工廠類,雖然看來去井然有序,但是當使用不當時會招致程式奔潰,那麼是什麼情況會發生呢?
我們先來分析上面的工廠類對外的兩個介面:
RegisterRole
註冊角色指標物件到工廠GetRole
從工廠獲取角色指標物件
難道是指標物件沒有釋放導致資源洩露?不,不是這個問題,我們也不必手動去釋放指標,因為上面的工廠是「單例模式」,它的生命週期是從第一次初始化後到程式結束,那麼程式結束後,作業系統自然就會回收工廠類裡的所有指標物件資源。
但是當我們手動去釋放從工廠獲取的角色指標物件,那麼就會有問題了:
RoleOperation* pRoleOperation = RoleFactory::Instance().GetRole(roleName); ... delete pRoleOperation; // 手動去釋放指標物件
如果我們手動釋放了指標物件,也就導致工廠裡 map 中存放的指標物件指向了空,當下次再次使用時,就會招致程式奔潰!如下面的例子:
class JudgeRole { public: std::string Judge(const std::string &roleName) { RoleOperation *pRoleOperation = RoleFactory::Instance().GetRole(roleName); std::string ret = pRoleOperation->Op(); delete pRoleOperation; // 手動去釋放指標物件 return ret; } }; int main() { InitializeRole(); // 優先初始化所有角色到工廠 JudgeRole judgeRole; std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; // 錯誤!程式會奔潰退出! return 0; }
上面的程式碼在使用第二次 ROLE_ROOT_ADMIN
角色指標物件時,就會招致程式奔潰,因為 ROLE_ROOT_ADMIN
角色指標物件已經在第一次使用完後,被手動釋放指標物件了,此時工廠 map 存放的就是空指標了。
可否優化呢?因為有的程式設計師是會手動釋放從工廠獲取的指標物件的。
上面的工廠類的缺陷就在於,new
初始化的指標物件只初始化了一次,如果手動 釋放了指標物件,就會導致此指標物件指向空,再次使用就會導致系統奔潰。
為了改進這個問題,那麼我們把 new
初始化方式放入工廠類獲取指標物件的成員函式裡,這也就每次呼叫該成員函式時,都是返回新 new
初始化過的指標物件,那麼這時外部就需要由手動釋放指標物件了。
下面的工廠類,改進了上面問題,同時採用模板技術,進一步對工廠類進行了封裝,使得不管是角色類,還是其他類,只要存在多型特性的類,都可以使用此工廠類,可以說是「萬能」的工廠類了:
接下來把新的「萬能」工廠模板類,使用到本例的角色物件。
1. 把角色註冊(聚合)到工廠的方式是構造 ProductRegistrar
物件 ,使用時需注意:
- 模板引數
ProductType_t
指定的是基類(如本例 RoleOperation ) - 模板引數
ProductImpl_t
指定的是派生類(如本例 RootAdminRole、OrderAdminRole 和 NormalRole)
我們使用新的註冊(聚合)方式,對 InitializeRole
初始化角色函式改進下,參見下面:
void InitializeRole() // 初始化角色到工廠 { static bool bInitialized = false; if (bInitialized == false) { // 註冊系統管理員 static ProductRegistrar<RoleOperation, RootAdminRole> rootRegistrar("ROLE_ROOT_ADMIN"); // 註冊訂單管理員 static ProductRegistrar<RoleOperation, OrderAdminRole> orderRegistrar("ROLE_ORDER_ADMIN"); // 註冊普通使用者 static ProductRegistrar<RoleOperation, NormalRole> normalRegistrar("ROLE_NORMAL"); bInitialized = true; } }
2. 從工廠獲取角色指標物件的函式是 GetProduct
,需注意的是:
- 使用完角色指標物件後,需手動
delete
資源。
我們使用新的獲取角色物件的方式,對 Judge
函式改進下,參見下面:
class JudgeRole { public: std::string Judge(const std::string &roleName) { ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance(); // 從工廠獲取對應的指標物件 RoleOperation *pRoleOperation = factory.GetProduct(roleName); // 呼叫角色的對應操作許可權 std::string result = pRoleOperation->Op(); // 手動釋放資源 delete pRoleOperation; return result; } };
唔,每次都手動釋放資源這種事情,會很容易遺漏。如果我們遺漏了,就會招致了記憶體洩漏。為了避免此概率事情的發生,我們用上「智慧指標],讓它幫我們管理吧:
class JudgeRole { public: std::string Judge(const std::string &roleName) { ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance(); std::shared_ptr<RoleOperation> pRoleOperation(factory.GetProduct(roleName)); return pRoleOperation->Op(); } };
採用了 std::shared_ptr
引用計數智慧指標,我們不在需要時刻記住要手動釋放資源的事情啦(我們通常都會忘記……),該智慧指標會在當引用次數為 0 時,自動會釋放掉指標資源。
來,我們接著來,除了工廠模式,策略模式也不妨試一試
策略模式 —— 它不香嗎?
策略模式和工廠模式寫起來其實區別也不大!策略模式也採用了面向物件的繼承和多型機制。
在上面工廠模式程式碼的基礎上,按照策略模式的指導思想,我們也來建立一個所謂的策略上下文類,這裡命名為 RoleContext
:
class RoleContext { public: RoleContext(RoleOperation *operation) : m_pOperation(operation) { } ~RoleContext() { if (m_pOperation) { delete m_pOperation; } } std::string execute() { return m_pOperation->Op(); } private: // 禁止外部拷貝和賦值操作 RoleContext(const RoleContext &); const RoleContext &operator=(const RoleContext &); RoleOperation *m_pOperation; };
很明顯上面傳入的引數 operation
就是表示不同的「策略」。我們在業務程式碼裡傳入不同的角色,即可得到不同的操作結果:
class JudgeRole { public: std::string Judge(RoleOperation *pOperation) { RoleContext roleContext(pOperation); return roleContext.execute(); } }; int main() { JudgeRole judgeRole; std::cout << judgeRole.Judge(new RootAdminRole("ROLE_ROOT_ADMIN")) << std::endl; std::cout << judgeRole.Judge(new OrderAdminRole("ROLE_ORDER_ADMIN")) << std::endl; std::cout << judgeRole.Judge(new NormalRole("ROLE_NORMAL")) << std::endl; return 0; }
當然,上面策略類還可以進一步優化:
- 用模板技術進一步封裝,使其不限制於角色類。
// 策略類模板 // 模板引數 ProductType_t,表示的是基類 template <class ProductType_t> class ProductContext { public: ProductContext(ProductType_t *operation) : m_pOperation(operation) { } ~ProductContext() { if (m_pOperation) { delete m_pOperation; } } std::string execute() { return m_pOperation->Op(); } private: // 禁止外部拷貝和賦值操作 ProductContext(const ProductContext &); const ProductContext &operator=(const ProductContext &); ProductType_t* m_pOperation; };
使用方式,沒太大差別,只需要指定類模板引數是基類(如本例 RoleOperation
) 即可:
class JudgeRole { public: std::string Judge(RoleOperation *pOperation) { ProductContext<RoleOperation> roleContext(pOperation); return roleContext.execute(); } };
共勉
C++ 和 Java 語言都是面向物件程式設計的方式,所以都是可以通過面向物件和多型特性降低程式碼的耦合性,同時也可使得程式碼易擴充套件。所以對於寫程式碼事情,不要著急下手,先思考是否有更簡單、更好的方式去實現。
C++ 之父 Bjarne Stroustrup 曾經提及過程式設計師的三大美德是懶惰、急躁、傲慢,其中之一的懶惰這個品質,就是告知我們要花大力氣去思考,避免消耗過多的精力個體力(如敲程式碼)。
若有錯誤或者不當之處,可在本公眾號內反饋,一起學習交流!
推薦閱讀:
- 掌握了多型的特性,寫英雄聯盟的程式碼更少啦!
- 學過 C++ 的你,不得不知的這 10 條細節!
關注公眾號,後臺回覆「我要學習」,即可免費獲取精心整理「伺服器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)