
前言 羊哥之前写一篇有趣的文章《答应我,别再if/else走天下了可以吗 | CodeSheep》,在文中使用 Java 语言实现了枚举类、工厂模式和策略模式的三种方式,来消除连环的if/else。内容层层递进,由浅入深的方式我非常喜欢。 看到有留言中有小伙伴想看 C++ 版本的,特此写下了此文(已经过羊哥的同意)。不过由于 C++ 没有枚举类,所以本文不涉及此方式,但本文会带大家一步一步的优化工厂模式和策略模式。 正文糟糕 if / else 连环if/else可以说是我们学习编程时,第一个学习的分支语句,简单易理解,生活中也处处有的if/else例子:
老婆的思维: 买一斤包子;2 Z$ ?, i+ `5 Oif( 看到卖西瓜的 ) 买一只( 西瓜 ); 而程序员老公的程序: if( ! 看见卖西瓜的 ) & I% N2 c* @8 ]% F& o7 d买一斤包子; else 买一只( 包子 ); 非常生生动动的生活例子!如果身为程序员的你,犯了同样的思维错误,别继续问你媳妇为什么,问就是跪键盘:
![]() 进入本文正题。考虑以下栗子:一般来说我们正常的后台管理系统都有所谓的角色的概念,不同管理员权限不一样,能够行使的操作也不一样。
假设一个用户进来,我们需要根据不同用户的角色来判断其有哪些行为。使用过多if/else连环写法的我们,肯定下意识就觉得,这不简单嘛,我上演一套连环的写法: class JudgeRole6 [3 q1 {4 L$ |" \{: E& |4 o; k3 D public: std::string Judge( std::string roleName )) _& A7 g7 y. u$ A# H- g5 R" x { std::string result = ""; if( roleName == "ROLE_ROOT_ADMIN" ) // 系统管理员+ }4 |! X, j6 k+ ~4 R { result = roleName + "has A permission";* H/ H5 G2 e% N, V8 s } else if( roleName == "ROLE_ORDER_ADMIN" ) // 订单管理员7 k* f" C& |' [ _( C- M% N! ~ { result = roleName + "has B permission";3 H0 k) T$ {: S" s3 h2 ^* [0 h+ M, ` }, g3 M# q+ K {, U2 O' x% H else if( roleName == "ROLE_NORMAL" ) // 普通用户" ~: r4 Z/ Z& j( L& `) T { result = roleName + "has C permission"; }" m g! z* X+ q T' m1 J6 w8 f( ? return result; }! W8 S: `9 P( h+ i* t6 E& @ }; 当系统里有几十个角色,那岂不是几十个if/else嵌套,这个视觉效果绝对酸爽……这种实现方式非常的不优雅。 别人看了这种代码肯定大声喊:“我X,哪个水货写的!” 这时你听到,千万不要说:“那我改成switch/case”。千万别说,千万别说哦,否则可能拎包回家了……
![]() 因为switch/case和if/else毛区别都没,都是写费劲、难阅读、不易扩展的代码。
工厂模式 —— 它不香吗? 不同的角色做不同的事情,很明显就提供了使用工厂模式的契机,我们只需要将不同情况单独定义好,并聚合到工厂里面即可。 首先,定义一个公用接口RoleOperation,类里有一个纯虚函数Op,供派生类(子类)具体实现: // 基类7 G k( W9 K) a2 h: Vclass RoleOperation { public:' C% Z; Z, J" H9 T; X% y7 U virtual std::string Op() = 0; // 纯虚函数 virtual ~RoleOperation() {} // 虚析构函数 };& h' ]4 U# X* D' [" e 接下来针对不同的角色类,继承基类,并实现 Op 函数: // 系统管理员(有 A 操作权限) I% Y; y+ C: w1 Nclass RootAdminRole : public RoleOperation {2 F, a' ~! n1 j' D# Y. b public: RootAdminRole(const std::string &roleName) : m_RoleName(roleName) {} F3 W. o( [% ]. Z$ }: P ) q% C* x% ?' M! R2 `1 }& w std::string Op() {2 b5 ^. U) Y$ M" u0 z. d return m_RoleName + " has A permission";8 F" z( D0 c% a" N- u4 W }6 ^7 h4 Y7 Q. f! s- U4 W- P 3 G/ K4 I8 \2 z0 m, G( b3 H private: B9 ^) J& r: E: j$ S! p std::string m_RoleName;" p1 `( \! L6 p; d: {3 a/ i& T2 N };/ s! W9 b8 m' J3 J) b5 { % w1 C! P- s6 T, ~* I% e // 订单管理员(有 B 操作权限) class OrderAdminRole : public RoleOperation {1 n: B1 W% v' l/ }! ? public:6 ?9 q1 n, \& G: F OrderAdminRole(const std::string &roleName) : m_RoleName(roleName) {} : v5 y7 t) L( Q- `, r* p std::string Op() {. `6 ~( u" z! F) j( P* _7 t% @ return m_RoleName + " has B permission"; } private: std::string m_RoleName;0 S; n! {2 I% `/ l' s- S: N% A6 w0 P };/ }% n# ]' Z- W( P5 C; y% m5 @ H // 普通用户(有 C 操作权限) class NormalRole : public RoleOperation {+ F) s/ a2 B! N0 Z- M' @, b public:- i% S/ y# p2 B8 P2 k) e+ }4 X NormalRole(const std::string &roleName)" ]# S3 j! ]7 I; b: E : m_RoleName(roleName) {} 0 _; J6 \9 q* K" [ std::string Op() { return m_RoleName + " has C permission"; } private: std::string m_RoleName;8 }+ H; h* y3 E4 y }; 接下来在写一个工厂类RoleFactory,提供两个接口:
. c! N; d, p+ q // 角色工厂class RoleFactory {/ K8 f, E6 _4 W3 R U [ public: // 获取工厂单例,工厂的实例是唯一的 static RoleFactory& Instance() {) b: z5 | T( X/ `# F, Y1 M! ?' H static RoleFactory instance; // C++11 以上线程安全 return instance;- G3 @8 ?0 w3 S9 B } ! F; Q/ M& ^9 i9 s% b6 \: D1 a // 把指针对象注册到工厂 void RegisterRole(const std::string& name, RoleOperation* registrar) {9 j3 ^' a: {. c* U m_RoleRegistry[name] = registrar; } // 根据名字name,获取对应的角色指针对象 RoleOperation* GetRole(const std::string& name) { std::map<std::string, RoleOperation*>::iterator it; 6 V/ \( `3 G7 u$ I: Z // 从map找到已经注册过的角色,并返回角色指针对象 it = m_RoleRegistry.find(name);4 ?4 @( y3 }5 b' q if (it != m_RoleRegistry.end()) { return it->second; }) ^8 Q% m- K* P' E# v' E : z* h/ t8 Q1 M6 w( a return nullptr; // 未注册该角色,则返回空指针 } private: // 禁止外部构造和虚构 RoleFactory() {} ~RoleFactory() {} // 禁止外部拷贝和赋值操作 RoleFactory(const RoleFactory &);& N" H# J: X z* r/ P const RoleFactory &operator=(const RoleFactory &);9 C# u2 t, ~& F) j1 W. b9 i 9 F% }9 @# D; Z& f) z // 保存注册过的角色,key:角色名称 , value:角色指针对象1 W. v* I/ _; ?6 b std::map<std::string, RoleOperation *> m_RoleRegistry; };# U b. a, n& M3 y0 t 把所有的角色注册(聚合)到工厂里,并封装成角色初始化函InitializeRole: void InitializeRole() // 初始化角色到工厂{ static bool bInitialized = false;" q" t) i0 o" H0 h! b5 {8 w if (bInitialized == false) {( y* E) J" D8 l; a% V; n% U // 注册系统管理员+ J$ }3 n7 _% Z RoleFactory::Instance().RegisterRole("ROLE_ROOT_ADMIN", new RootAdminRole("ROLE_ROOT_ADMIN")); // 注册订单管理员 RoleFactory::Instance().RegisterRole("ROLE_ORDER_ADMIN", new OrderAdminRole("ROLE_ORDER_ADMIN")); // 注册普通用户3 `4 T3 h! U& \2 b# X RoleFactory::Instance().RegisterRole("ROLE_NORMAL", new NormalRole("ROLE_NORMAL"));9 B" o3 T! Q. k3 S2 V! x bInitialized = true;5 h1 K; I/ h3 L1 \ }/ G6 {3 e5 w8 i$ @; Q } 接下来借助上面这个工厂,业务代码调用只需要一行代码,if/else被消除的明明白白: class JudgeRole {* H- U) V7 e# ?$ p6 c2 hpublic: std::string Judge(const std::string &roleName) { return RoleFactory::Instance().GetRole(roleName)->Op();7 H# v* N; T, x" n4 j. d+ b0 x }1 y6 y: H3 ]% e };0 c1 F# `+ r- v6 d r1 ?$ t& P 需要注意:在使用Judge时,要先调用初始化所有角色 InitializeRole 函数(可以放在main函数开头等): int main() {* J, V3 f1 ` ^# Y3 TInitializeRole(); // 优先初始化所有角色到工厂 JudgeRole judgeRole;5 J, {- X- C+ [9 Q$ |- [- o std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; std::cout << judgeRole.Judge("ROLE_ORDER_ADMIN") << std::endl;) Q9 [7 p* j# p5 ^5 e# t+ k6 B3 _ std::cout << judgeRole.Judge("ROLE_NORMAL") << std::endl;/ Q6 p5 o) |: _' p. K }2 b2 B$ U9 o3 V0 ?: ~ 通过工厂模式实现的方式,想扩展条件也很容易,只需要增加新代码,而不需要改动以前的业务代码,非常符合「开闭原则」。
![]()
我们先来分析上面的工厂类对外的两个接口:
难道是指针对象没有释放导致资源泄露?不,不是这个问题,我们也不必手动去释放指针,因为上面的工厂是「单例模式」,它的生命周期是从第一次初始化后到程序结束,那么程序结束后,操作系统自然就会回收工厂类里的所有指针对象资源。 但是当我们手动去释放从工厂获取的角色指针对象,那么就会有问题了: RoleOperation* pRoleOperation = RoleFactory::Instance().GetRole(roleName);... delete pRoleOperation; // 手动去释放指针对象! ]! R0 ~/ I5 {: j# Y4 v) ^, T 如果我们手动释放了指针对象,也就导致工厂里 map 中存放的指针对象指向了空,当下次再次使用时,就会招致程序奔溃!如下面的例子: class JudgeRole {public:/ l/ J/ `6 G) S \+ g std::string Judge(const std::string &roleName) {! O' T, A" _. C8 ? N4 F# D; l; q* _5 l RoleOperation *pRoleOperation = RoleFactory::Instance().GetRole(roleName); std::string ret = pRoleOperation->Op(); delete pRoleOperation; // 手动去释放指针对象 return ret;3 [0 O) g: s/ _2 j% x } }; 3 K- q* n/ p0 \1 L @+ w9 p int main() { InitializeRole(); // 优先初始化所有角色到工厂6 a$ s( C+ C! w: R j" S JudgeRole judgeRole;8 `6 E7 D: \ p8 ?- c" O5 [3 j! x4 @/ ^ std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl;' p" J7 D; @7 ? std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; // 错误!程序会奔溃退出! # g8 ~8 |- m8 H3 m6 T6 B! ^( P/ B return 0; } 上面的代码在使用第二次ROLE_ROOT_ADMIN角色指针对象时,就会招致程序奔溃,因为ROLE_ROOT_ADMIN角色指针对象已经在第一次使用完后,被手动释放指针对象了,此时工厂map存放的就是空指针了。
上面的工厂类的缺陷就在于,new初始化的指针对象只初始化了一次,如果手动 释放了指针对象,就会导致此指针对象指向空,再次使用就会导致系统奔溃。 为了改进这个问题,那么我们把 new初始化方式放入工厂类获取指针对象的成员函数里,这也就每次调用该成员函数时,都是返回新new初始化过的指针对象,那么这时外部就需要由手动释放指针对象了。 下面的工厂类,改进了上面问题,同时采用模板技术,进一步对工厂类进行了封装,使得不管是角色类,还是其他类,只要存在多态特性的类,都可以使用此工厂类,可以说是「万能」的工厂类了:
![]() 接下来把新的「万能」工厂模板类,使用到本例的角色对象。 1. 把角色注册(聚合)到工厂的方式是构造ProductRegistrar对象 ,使用时需注意:
我们使用新的注册(聚合)方式,对InitializeRole初始化角色函数改进下,参见下面: void InitializeRole() // 初始化角色到工厂0 y1 n! [- d' ~# j- d# d& ?{ static bool bInitialized = false;, d3 M$ p+ x- t if (bInitialized == false) { // 注册系统管理员" x; z* ~& x, G" M# | static ProductRegistrar<RoleOperation, RootAdminRole> rootRegistrar("ROLE_ROOT_ADMIN"); // 注册订单管理员 static ProductRegistrar<RoleOperation, OrderAdminRole> orderRegistrar("ROLE_ORDER_ADMIN");6 h: m2 ^5 M: _( p6 A' N$ O3 m. z // 注册普通用户- d1 r6 g+ z3 k( N: ~ static ProductRegistrar<RoleOperation, NormalRole> normalRegistrar("ROLE_NORMAL"); bInitialized = true;) s- G7 g* W7 \# @, M }2 K7 c z+ K3 ~, E, r" J }# z8 {4 a: ?1 n: c: u' b" C' R' ~ 2. 从工厂获取角色指针对象的函数是GetProduct,需注意的是:
我们使用新的获取角色对象的方式,对Judge函数改进下,参见下面: class JudgeRole {; k2 X$ W& z! g1 Zpublic: std::string Judge(const std::string &roleName) { ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance(); // 从工厂获取对应的指针对象7 d/ q7 k" A0 ^9 p6 F RoleOperation *pRoleOperation = factory.GetProduct(roleName); // 调用角色的对应操作权限# d7 e# h/ M% ^5 x std::string result = pRoleOperation->Op(); // 手动释放资源, D" ?' x% l S3 l. ?- N! v delete pRoleOperation;5 G0 j# `2 B! I% [8 S- y' o9 Z return result;( N' O$ q9 U a: ?4 J- K9 { } }; 唔,每次都手动释放资源这种事情,会很容易遗漏。如果我们遗漏了,就会招致了内存泄漏。为了避免此概率事情的发生,我们用上「智能指针],让它帮我们管理吧: class JudgeRole {public:. E0 e' b3 r, Q. N: O std::string Judge(const std::string &roleName) { ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance(); std::shared_ptr<RoleOperation> pRoleOperation(factory.GetProduct(roleName)); return pRoleOperation->Op(); } };' {2 J& v2 v: I/ S+ G 采用了std::shared_ptr引用计数智能指针,我们不在需要时刻记住要手动释放资源的事情啦(我们通常都会忘记……),该智能指针会在当引用次数为 0 时,自动会释放掉指针资源。
策略模式 —— 它不香吗? 策略模式和工厂模式写起来其实区别也不大!策略模式也采用了面向对象的继承和多态机制。 ![]() 在上面工厂模式代码的基础上,按照策略模式的指导思想,我们也来创建一个所谓的策略上下文类,这里命名为RoleContext: class RoleContext {# ~! e! t7 R/ U5 h) o6 Ypublic: RoleContext(RoleOperation *operation) : m_pOperation(operation) {) @$ K9 f. {2 q; k) R4 S0 K% D }% C8 P) e0 Z# Q0 S ) l1 y6 O8 `- L& u5 \: ]6 `$ F6 a ~RoleContext() { if (m_pOperation) {0 b+ D1 g+ z, } delete m_pOperation;7 @! m& n# q$ D* Q5 C1 ]% b# g }, u6 B8 Y: x/ r) C } # M3 P2 e" j% j- m# e C5 M std::string execute() {0 G; K T" s. r2 f return m_pOperation->Op(); } private:; o4 N3 K5 x9 r6 r1 ?; B3 D: _5 k // 禁止外部拷贝和赋值操作 RoleContext(const RoleContext &); const RoleContext &operator=(const RoleContext &);0 ~1 M! g1 V; E/ E# O RoleOperation *m_pOperation; }; 很明显上面传入的参数operation就是表示不同的「策略」。我们在业务代码里传入不同的角色,即可得到不同的操作结果: class JudgeRole {public: std::string Judge(RoleOperation *pOperation) { RoleContext roleContext(pOperation); return roleContext.execute(); }; v/ L5 f5 x* \) F& j };: O A8 n4 G2 C" w" [$ [7 u0 F. n. f ! f# w% g8 C# }9 `9 u 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;( ~8 @/ k8 y; h+ P: \) W std::cout << judgeRole.Judge(new NormalRole("ROLE_NORMAL")) << std::endl;! f: V1 I- v. ? 1 C2 D, e- m1 N( M return 0;% h# w! u9 Y( m/ H) j }; x$ c: h) K- @& E$ R+ P2 j2 B // 策略类模板* f1 v3 }7 n" i! H" A" a9 i // 模板参数 ProductType_t,表示的是基类 template <class ProductType_t>4 G% h5 m0 I( d/ ]$ O8 C class ProductContext {' S4 v% v, U. I6 o/ R public: ProductContext(ProductType_t *operation) : m_pOperation(operation) { } ~ProductContext() {3 S+ `1 X/ s: F4 M# A7 Q if (m_pOperation) {- ~. H, }& o5 r7 z delete m_pOperation;* }& x1 G6 y; u# \# t } } std::string execute() {! Q2 x y$ ~4 E/ B% c6 p return m_pOperation->Op();' J5 \1 i) b- ?" K' |. e }! ]3 S4 p$ L: ?3 h+ h7 s private: // 禁止外部拷贝和赋值操作 ProductContext(const ProductContext &);; P& s4 `2 D- j+ f' U7 E& ? const ProductContext &operator=(const ProductContext &); . W# }- L) W* k K7 {: y ProductType_t* m_pOperation; }; 使用方式,没太大差别,只需要指定类模板参数是基类(如本例 RoleOperation) 即可: class JudgeRole {public: std::string Judge(RoleOperation *pOperation) { ProductContext<RoleOperation> roleContext(pOperation); return roleContext.execute(); }8 Y/ _# ]" O* E# Y- O) w }; 共勉 C++ 和 Java 语言都是面向对象编程的方式,所以都是可以通过面向对象和多态特性降低代码的耦合性,同时也可使得代码易扩展。所以对于写代码事情,不要着急下手,先思考是否有更简单、更好的方式去实现。 C++ 之父 Bjarne Stroustrup 曾经提及过程序员的三大美德是懒惰、急躁、傲慢,其中之一的懒惰这个品质,就是告知我们要花大力气去思考,避免消耗过多的精力个体力(如敲代码)。 / ]' i: V0 j0 N |