每日一句英語學習,每天進步一點點:
- "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
買一隻( 包子 );
非常生生動動的生活例子!如果身為程式員的你,犯了同樣的思維錯誤,别繼續問你媳婦為什麼,問就是跪鍵盤:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yMwcjMjhTMiZGOiR2M3YWOkRWOjJDNjJ2NzU2NkNDZi9CX2AzLclDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL2M3Lc9CX6MHc0RHaiojIsJye.png)
進入本文正題。考慮以下栗子:一般來說我們正常的背景管理系統都有所謂的角色的概念,不同管理者權限不一樣,能夠行使的操作也不一樣。
- 系統管理者(
):有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
對象 ,使用時需注意:
- 模闆參數
指定的是基類(如本例 RoleOperation )ProductType_t
-
指定的是派生類(如本例 RootAdminRole、OrderAdminRole 和 NormalRole)ProductImpl_t
我們使用新的注冊(聚合)方式,對
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 曾經提及過程式員的三大美德是懶惰、急躁、傲慢,其中之一的懶惰這個品質,就是告知我們要花大力氣去思考,避免消耗過多的精力個體力(如敲代碼)。