你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C++ 版本——优化工厂模式和策略模式

[复制链接]
gaosmile 发布时间:2020-7-9 12:50
前言
羊哥之前写一篇有趣的文章《答应我,别再if/else走天下了可以吗 | CodeSheep》,在文中使用 Java 语言实现了枚举类、工厂模式和策略模式的三种方式,来消除连环的if/else。内容层层递进,由浅入深的方式我非常喜欢。
看到有留言中有小伙伴想看 C++ 版本的,特此写下了此文(已经过羊哥的同意)。不过由于 C++ 没有枚举类,所以本文不涉及此方式,但本文会带大家一步一步的优化工厂模式策略模式
正文糟糕 if / else 连环
if/else可以说是我们学习编程时,第一个学习的分支语句,简单易理解,生活中也处处有的if/else例子:
老婆给当程序员的老公打电话:“下班顺路买一斤包子带回来,如果看到卖西瓜的,买一个。”  
当晚,程序员老公手捧一个包子进了家门。。。
老婆怒道:“你怎么就买了一个包子?!”
老公答曰:“因为看到了卖西瓜的。”
老婆的思维:
买一斤包子;
if( 看到卖西瓜的 )
  买一只( 西瓜 );

而程序员老公的程序:
if( ! 看见卖西瓜的 )
   买一斤包子;
else
   买一只( 包子 );

非常生生动动的生活例子!如果身为程序员的你,犯了同样的思维错误,别继续问你媳妇为什么,问就是跪键盘
微信图片_20200709124825.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”。千万别说,千万别说哦,否则可能拎包回家了……
微信图片_20200709124828.jpg
因为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;
}

通过工厂模式实现的方式,想扩展条件也很容易,只需要增加新代码,而不需要改动以前的业务代码,非常符合「开闭原则」。
微信图片_20200709124831.jpg
不知道小伙伴发现了没有,上面实现工厂类,虽然看来去井然有序,但是当使用不当时会招致程序奔溃,那么是什么情况会发生呢?
我们先来分析上面的工厂类对外的两个接口:
  • 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初始化过的指针对象,那么这时外部就需要由手动释放指针对象了
下面的工厂类,改进了上面问题,同时采用模板技术,进一步对工厂类进行了封装,使得不管是角色类,还是其他类,只要存在多态特性的类,都可以使用此工厂类,可以说是「万能」的工厂类了:
微信图片_20200709124834.jpg 「万能」工厂
接下来把新的「万能」工厂模板类,使用到本例的角色对象。
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 时,自动会释放掉指针资源
来,我们接着来,除了工厂模式,策略模式也不妨试一试

策略模式 —— 它不香吗?
策略模式和工厂模式写起来其实区别也不大!策略模式也采用了面向对象的继承和多态机制
微信图片_20200709124838.gif
在上面工厂模式代码的基础上,按照策略模式的指导思想,我们也来创建一个所谓的策略上下文类,这里命名为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 曾经提及过程序员的三大美德是懒惰、急躁、傲慢,其中之一的懒惰这个品质,就是告知我们要花大力气去思考,避免消耗过多的精力个体力(如敲代码)。

收藏 评论0 发布时间:2020-7-9 12:50

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版