「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() = ; // 纯虚函数
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 ;
}
上面的代码在使用第二次 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 ;
}
当然,上面策略类还可以进一步优化:
- 用模板技术进一步封装,使其不限制于角色类。
// 策略类模板
// 模板参数 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++ 」成长路程(书籍资料 + 思维导图)

「C++ 篇」答应我,别再if/else走天下了可以吗的更多相关文章
- PHP丨PHP基础知识之PHP基础入门——函数「理论篇」
前两天讲过PHP基础知识的判断条件和流程控制,今天来讲讲PHP基础知识之PHP基础入门--函数! 一.函数的声明与使用 1.函数名是标识符之一,只能有数字字母下划线,开头不能是数字. 函数名的命名,须 ...
- PHP丨PHP基础知识之流程控制WHILE循环「理论篇」
昨天讲完FOR循环今天来讲讲他的兄弟WHILE循环!进入正题: while是计算机的一种基本循环模式.当满足条件时进入循环,进入循环后,当条件不满足时,跳出循环.while语句的一般表达式为:whil ...
- PHP丨PHP基础知识之条件语IF判断「理论篇」
if语句是指编程语言(包括c语言.C#.VB.java.php.汇编语言等)中用来判定所给定的条件是否满足,根据判定的结果(真或假)决定执行给出的两种操作之一. if语句概述 if语句是指编程语言(包 ...
- PHP丨PHP基础知识之条件语SWITCH判断「理论篇」
Switch在一些计算机语言中是保留字,其作用大多情况下是进行判断选择.以PHP来说,switch(开关语句)常和case break default一起使用 典型结构 switch($control ...
- 「入门篇」初识JVM
记录于 2022-01-02 17:25:12 GhostFace 1. 什么是JVM? 概念 来自百度百科 JVM是 Java Virtual Machine(Java虚拟机)的缩写,JVM是一 ...
- 「进阶篇」Vue Router 核心原理解析
前言 此篇为进阶篇,希望读者有 Vue.js,Vue Router 的使用经验,并对 Vue.js 核心原理有简单了解: 不会大篇幅手撕源码,会贴最核心的源码,对应的官方仓库源码地址会放到超上,可以配 ...
- 「 JavaScript 篇 」
一.JavaScript 里有哪些数据类型,解释清楚 null 和 undefined,解释清楚原始数据类型和引用数据类型.比如讲一下 1 和 Number(1)的区别js中有5种数据类型:Undef ...
- 二十三、详述 IntelliJ IDEA 中恢复代码的方法「进阶篇」
咱们已经了解了如何将代码恢复至某一版本,但是通过Local History恢复代码有的时候并不方便,例如咱们将项目中的代码进行了多处修改,这时通过Local History恢复代码就显得很麻烦,因为它 ...
- PHP丨PHP基础知识之流程控制for循环「理论篇」
今天公司同事在看for循环,那么我们今天就来讲讲for循环吧! for循环是编程语言中一种循环语句,而循环语句由循环体及循环的判定条件两部分组成,其表达式为:for(单次表达式;条件表达式;末尾循环体 ...
随机推荐
- 解决vue中element组件样式修改无效
vue中element组件样式修改无效 <style> .detail{ .el-input__inner { height: 48px; } } </style> 直接写st ...
- [JavaScript设计模式]惰性单例模式
惰性单例模式 之前介绍了JS中类的单例模式,这次我们讨论下单例模式的应用.在众多网站中,登录框的实现方式就是一个单例,点击一次就展示一次,所以我们可以在页面加载好的时候就创建一个登录框,点击页面上的登 ...
- Could not find iPhone 6 simulator
最近原来的老项目有点问题需要处理一下,运行启动命令,就报了如下错误,提示找不到iPhone 6 模拟器. react-native run-ios Owaiss-Mac:pdm owaisahmed$ ...
- cogs 1176. [郑州101中学] 月考 字典树
1176. [郑州101中学] 月考 ★★☆ 输入文件:mtest.in 输出文件:mtest.out 简单对比时间限制:1 s 内存限制:128 MB [题目描述] 在上次的月考中B ...
- Java框架之Spring02-AOP-动态代理-AspectJ-JdbcTemplate-事务
AOP 动态代理 代理设计模式的原理:使用一个代理将原本对象包装起来,然后用该代理对象”取代”原始对象.任何对原始对象的调用都要通过代理.代理对象决定是否以及何时将方法调用转到原始对象上. 代理模式的 ...
- 多级反向代理java获取真实IP地址
public static String getIpAddress(HttpServletRequest request){ String ip = request.getHeader("x ...
- 《ASP.NET Core 高性能系列》关于性能的闲聊
一.通常的性能问题类型 让我们一起看看那些公共的性能问题,看看他们是或者不是.我们将了解到为什么我们常常在开发期间会错过这些问题.我们也会看看当我们考虑性能时语言的选择.延迟.带宽.计算等因素. 二. ...
- 【Java并发基础】管程简介
前言 在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的.除了Java之外,C/C++.C#等高级语言也都是支持管程的. 那么什么 ...
- 基于Bootstrap和Knockout.js的ASP.NET MVC开发实战
之前在一家公司里用过Knockout,是easyui 和 Knockout结合 的.下面的这本应该不错. 目录 前言 第一部分入门指南 第1章MVC介绍 创建第一个项目 分析HomeControlle ...
- SSH免密登录设置步骤
1.配置公钥:执行ssh-keygen即可生成SSH钥匙,一路回车即可 ssh-keygen 2.上传公钥到服务器:执行 ssh-copy-id -p port user@remote,可以让远程服务 ...