「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 / caseif / 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 曾經提及過程序員的三大美德是懶惰、急躁、傲慢,其中之一的懶惰這個品質,就是告知我們要花大力氣去思考,避免消耗過多的精力個體力(如敲代碼)。

若有錯誤或者不當之處,可在本公眾號內反饋,一起學習交流!


推薦閱讀:

關注公眾號,后台回復「我要學習」,即可免費獲取精心整理「服務器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM