
前言 羊哥之前写一篇有趣的文章《答应我,别再if/else走天下了可以吗 | CodeSheep》,在文中使用 Java 语言实现了枚举类、工厂模式和策略模式的三种方式,来消除连环的if/else。内容层层递进,由浅入深的方式我非常喜欢。 看到有留言中有小伙伴想看 C++ 版本的,特此写下了此文(已经过羊哥的同意)。不过由于 C++ 没有枚举类,所以本文不涉及此方式,但本文会带大家一步一步的优化工厂模式和策略模式。 正文糟糕 if / else 连环if/else可以说是我们学习编程时,第一个学习的分支语句,简单易理解,生活中也处处有的if/else例子:
老婆的思维: 买一斤包子;if( 看到卖西瓜的 ) 买一只( 西瓜 );5 c& q; [( a2 p) E; O 而程序员老公的程序: if( ! 看见卖西瓜的 ) 8 C. t" R4 j: m. T! e买一斤包子; else 买一只( 包子 ); 非常生生动动的生活例子!如果身为程序员的你,犯了同样的思维错误,别继续问你媳妇为什么,问就是跪键盘:
![]() 进入本文正题。考虑以下栗子:一般来说我们正常的后台管理系统都有所谓的角色的概念,不同管理员权限不一样,能够行使的操作也不一样。
假设一个用户进来,我们需要根据不同用户的角色来判断其有哪些行为。使用过多if/else连环写法的我们,肯定下意识就觉得,这不简单嘛,我上演一套连环的写法: class JudgeRole{) \: j9 r: }8 n6 U' k& }; [) U public: std::string Judge( std::string roleName ) {+ o+ E$ G. Y- N: _ std::string result = "";& K; e+ z. d" M: G9 w/ a if( roleName == "ROLE_ROOT_ADMIN" ) // 系统管理员# `) _& J. ^5 Q" J+ o9 Q {* S; N: q( @+ T( }" z result = roleName + "has A permission"; }/ R# _- a5 O$ B) l4 P else if( roleName == "ROLE_ORDER_ADMIN" ) // 订单管理员 { result = roleName + "has B permission"; } else if( roleName == "ROLE_NORMAL" ) // 普通用户 { result = roleName + "has C permission";2 W( k1 j; |- N9 F }0 P7 [* _$ q1 o# S. `) d# K return result;# d8 U! L8 c8 l7 k8 J }3 ?1 `9 t' R: k1 R7 L };& R3 f! ^7 N: k3 G A( Y ] 当系统里有几十个角色,那岂不是几十个if/else嵌套,这个视觉效果绝对酸爽……这种实现方式非常的不优雅。 别人看了这种代码肯定大声喊:“我X,哪个水货写的!” 这时你听到,千万不要说:“那我改成switch/case”。千万别说,千万别说哦,否则可能拎包回家了……
![]() 因为switch/case和if/else毛区别都没,都是写费劲、难阅读、不易扩展的代码。
工厂模式 —— 它不香吗? 不同的角色做不同的事情,很明显就提供了使用工厂模式的契机,我们只需要将不同情况单独定义好,并聚合到工厂里面即可。 首先,定义一个公用接口RoleOperation,类里有一个纯虚函数Op,供派生类(子类)具体实现: // 基类class RoleOperation { public: virtual std::string Op() = 0; // 纯虚函数 virtual ~RoleOperation() {} // 虚析构函数, E$ N$ X( [3 g: u. w! y3 I3 n2 C }; 接下来针对不同的角色类,继承基类,并实现 Op 函数: // 系统管理员(有 A 操作权限)- l* V8 F2 W" g9 U: t& Kclass RootAdminRole : public RoleOperation { public:0 I9 u5 I ?3 i) E8 I' P+ U9 B, `3 W. z6 e RootAdminRole(const std::string &roleName) : m_RoleName(roleName) {}+ ~. Q' }0 u8 L& Q. G& E std::string Op() {5 \2 [+ U" B; j$ x5 s# n+ p return m_RoleName + " has A permission";# B* w, E- J- ]9 N$ o } private:: z/ Q5 J7 ^* ]6 ^2 w$ ? std::string m_RoleName; };+ F2 F9 n' P5 ]' Q9 _' B # w- c- I. S' A% U* h7 m // 订单管理员(有 B 操作权限)* ]8 R% H: k) |- T$ U class OrderAdminRole : public RoleOperation { public: OrderAdminRole(const std::string &roleName), b" [) C& t: ]' R : m_RoleName(roleName) {}8 D e: \ R1 O ~$ h+ F4 ]; @& K std::string Op() {2 d1 ~2 K5 Q' Y return m_RoleName + " has B permission";$ s' E0 o; e: O1 Q* P0 l' E9 L/ K } 0 e# }7 P; d* A+ G0 }+ F private:+ |0 O1 |* U# E5 A std::string m_RoleName; };8 u. T& g$ ^/ O& N! G 4 D6 B, J3 @0 [$ \3 n3 I4 | // 普通用户(有 C 操作权限)% n, V9 \ [5 ^+ o0 v) p& \3 q class NormalRole : public RoleOperation {4 [& E6 s. \ l! O% {1 h* {$ L* U2 c/ Y public:; x u2 ~' J+ c! z NormalRole(const std::string &roleName), [6 G8 l9 {& x) f0 F: k; e : m_RoleName(roleName) {} std::string Op() { return m_RoleName + " has C permission";/ Z7 [& X2 O/ x9 |& B: S }( Z* v' ^7 H' Y+ k1 N+ b0 n private: std::string m_RoleName; };" O) C( m( n% C# q 接下来在写一个工厂类RoleFactory,提供两个接口:
class RoleFactory { public:5 A% r6 ^) R4 T7 j. x' { // 获取工厂单例,工厂的实例是唯一的7 `! A; y2 l. x: a2 l) @ static RoleFactory& Instance() {6 V9 d. k& E6 ^ static RoleFactory instance; // C++11 以上线程安全 return instance;' P9 N* j& U& i! p/ N, _+ U }) n+ n7 P! F6 T% l) y& h |/ z // 把指针对象注册到工厂4 Y! s8 \( J# |( n% i0 q void RegisterRole(const std::string& name, RoleOperation* registrar) { m_RoleRegistry[name] = registrar; } 7 g% R0 W& s8 Y6 y // 根据名字name,获取对应的角色指针对象- \# T4 X+ A; W1 J5 ?7 x5 F" y* a RoleOperation* GetRole(const std::string& name) {/ k @" j$ E7 z8 f* z4 [ std::map<std::string, RoleOperation*>::iterator it;$ Q1 q$ [# b, V' s& ? // 从map找到已经注册过的角色,并返回角色指针对象6 n* S6 J2 Q8 C it = m_RoleRegistry.find(name); if (it != m_RoleRegistry.end()) {: ] M. o( w8 v- {' F/ w return it->second; } / J- M" z& k2 C7 q4 s return nullptr; // 未注册该角色,则返回空指针/ ^; H' R* j( s3 Z }5 o; h$ W3 j4 e. q" D8 Z * `4 Z w% E1 ]3 |3 H private:9 [0 W9 e5 y- @; [% ]8 f3 C // 禁止外部构造和虚构) H9 Q: r0 |1 Q9 p RoleFactory() {} ~RoleFactory() {} // 禁止外部拷贝和赋值操作' T. q: ^) m. K/ i; N RoleFactory(const RoleFactory &); const RoleFactory &operator=(const RoleFactory &);" B' C- V, h, z( _- R/ w+ k // 保存注册过的角色,key:角色名称 , value:角色指针对象1 i& e, v# O N3 A* r* I std::map<std::string, RoleOperation *> m_RoleRegistry;9 f k2 [9 J* R: ^ };( h- N3 r# `3 @# w F" f, D+ p 把所有的角色注册(聚合)到工厂里,并封装成角色初始化函InitializeRole: void InitializeRole() // 初始化角色到工厂{3 g5 W( C& q. n2 E- H5 J6 E static bool bInitialized = false;6 {# k- l0 Z5 a0 y$ X$ ` if (bInitialized == false) {( G2 o, R; N, i6 f7 M // 注册系统管理员; D+ p4 x( { ` RoleFactory::Instance().RegisterRole("ROLE_ROOT_ADMIN", new RootAdminRole("ROLE_ROOT_ADMIN"));$ A5 p: W% Y/ e0 T // 注册订单管理员 RoleFactory::Instance().RegisterRole("ROLE_ORDER_ADMIN", new OrderAdminRole("ROLE_ORDER_ADMIN"));. g# n3 U& a' j2 c0 @ // 注册普通用户 RoleFactory::Instance().RegisterRole("ROLE_NORMAL", new NormalRole("ROLE_NORMAL"));6 t, \ Q4 H4 H6 m; ]. L. z bInitialized = true;1 ]( a3 s/ o! f! [ } }1 D& C4 M% `7 D1 U C 接下来借助上面这个工厂,业务代码调用只需要一行代码,if/else被消除的明明白白: class JudgeRole {public:( x, t! H; [/ x9 ^$ p" B5 y7 X9 c std::string Judge(const std::string &roleName) {8 Z v/ H& @& m$ i return RoleFactory::Instance().GetRole(roleName)->Op(); } }; 需要注意:在使用Judge时,要先调用初始化所有角色 InitializeRole 函数(可以放在main函数开头等): int main() {! V9 }% I6 S* ?0 N# [8 L, m, UInitializeRole(); // 优先初始化所有角色到工厂* Z, S" h& Z; W8 E2 s0 U! @ JudgeRole judgeRole;6 y7 I! k9 o4 N7 q& U std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; v6 i6 v5 Y4 x std::cout << judgeRole.Judge("ROLE_ORDER_ADMIN") << std::endl; std::cout << judgeRole.Judge("ROLE_NORMAL") << std::endl; }$ \3 O0 R* C% u5 ~4 I, [, b 通过工厂模式实现的方式,想扩展条件也很容易,只需要增加新代码,而不需要改动以前的业务代码,非常符合「开闭原则」。
![]()
我们先来分析上面的工厂类对外的两个接口:
难道是指针对象没有释放导致资源泄露?不,不是这个问题,我们也不必手动去释放指针,因为上面的工厂是「单例模式」,它的生命周期是从第一次初始化后到程序结束,那么程序结束后,操作系统自然就会回收工厂类里的所有指针对象资源。 但是当我们手动去释放从工厂获取的角色指针对象,那么就会有问题了: RoleOperation* pRoleOperation = RoleFactory::Instance().GetRole(roleName);; Q7 k- G, k; l. N& }...* g- b6 v+ \9 S delete pRoleOperation; // 手动去释放指针对象 如果我们手动释放了指针对象,也就导致工厂里 map 中存放的指针对象指向了空,当下次再次使用时,就会招致程序奔溃!如下面的例子: class JudgeRole {+ R* D4 e7 W m: y. d0 Z7 T; G0 zpublic: std::string Judge(const std::string &roleName) { RoleOperation *pRoleOperation = RoleFactory::Instance().GetRole(roleName); std::string ret = pRoleOperation->Op();. ~, `& ]+ x& q delete pRoleOperation; // 手动去释放指针对象 return ret; }/ z" B) \+ y. v# r* d };& b6 }/ V( S% Q$ g int main() {/ ~3 H( D1 V4 }% @* A3 R2 `8 K. s InitializeRole(); // 优先初始化所有角色到工厂 JudgeRole judgeRole;3 C1 K9 B, F8 G M0 M }: P std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl;/ p, E( i0 O: r" x7 m ]9 i" u" u1 w std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; // 错误!程序会奔溃退出!1 |6 c' P% ] m return 0; } 上面的代码在使用第二次ROLE_ROOT_ADMIN角色指针对象时,就会招致程序奔溃,因为ROLE_ROOT_ADMIN角色指针对象已经在第一次使用完后,被手动释放指针对象了,此时工厂map存放的就是空指针了。
上面的工厂类的缺陷就在于,new初始化的指针对象只初始化了一次,如果手动 释放了指针对象,就会导致此指针对象指向空,再次使用就会导致系统奔溃。 为了改进这个问题,那么我们把 new初始化方式放入工厂类获取指针对象的成员函数里,这也就每次调用该成员函数时,都是返回新new初始化过的指针对象,那么这时外部就需要由手动释放指针对象了。 下面的工厂类,改进了上面问题,同时采用模板技术,进一步对工厂类进行了封装,使得不管是角色类,还是其他类,只要存在多态特性的类,都可以使用此工厂类,可以说是「万能」的工厂类了:
![]() 接下来把新的「万能」工厂模板类,使用到本例的角色对象。 1. 把角色注册(聚合)到工厂的方式是构造ProductRegistrar对象 ,使用时需注意:
我们使用新的注册(聚合)方式,对InitializeRole初始化角色函数改进下,参见下面: void InitializeRole() // 初始化角色到工厂{ static bool bInitialized = false; if (bInitialized == false) { // 注册系统管理员: S$ @3 n* H% ~ static ProductRegistrar<RoleOperation, RootAdminRole> rootRegistrar("ROLE_ROOT_ADMIN");3 M. u6 t& b0 w5 v( ~ // 注册订单管理员 static ProductRegistrar<RoleOperation, OrderAdminRole> orderRegistrar("ROLE_ORDER_ADMIN");* f7 j0 E% V" e3 v // 注册普通用户5 V8 S J5 t% ?- b3 O static ProductRegistrar<RoleOperation, NormalRole> normalRegistrar("ROLE_NORMAL"); bInitialized = true; } } 2. 从工厂获取角色指针对象的函数是GetProduct,需注意的是:
我们使用新的获取角色对象的方式,对Judge函数改进下,参见下面: class JudgeRole {( t- W, _: M, R( C! R/ J1 L! X2 y; lpublic: std::string Judge(const std::string &roleName) {# d' i/ S' j# P7 v; a1 g2 M ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance(); // 从工厂获取对应的指针对象- x( ]3 j; e' y/ N' `3 t/ m) ] RoleOperation *pRoleOperation = factory.GetProduct(roleName); // 调用角色的对应操作权限 std::string result = pRoleOperation->Op();) M, ~; \ S8 K+ h; S // 手动释放资源 delete pRoleOperation;: P3 ?2 q+ p4 `/ u6 k9 D return result;; K3 G! u' V8 A% C3 R$ l }% w9 }8 Q8 X& B( g! n };/ v7 U6 b, n+ a+ U2 H; B: B 唔,每次都手动释放资源这种事情,会很容易遗漏。如果我们遗漏了,就会招致了内存泄漏。为了避免此概率事情的发生,我们用上「智能指针],让它帮我们管理吧: class JudgeRole {1 T- I2 w/ w5 K8 Mpublic:$ _$ Q$ w' \1 E! N$ ? std::string Judge(const std::string &roleName) { ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance();/ a" v% G. m- A5 Y std::shared_ptr<RoleOperation> pRoleOperation(factory.GetProduct(roleName)); return pRoleOperation->Op(); } };; h$ ?# x0 X% r6 g 采用了std::shared_ptr引用计数智能指针,我们不在需要时刻记住要手动释放资源的事情啦(我们通常都会忘记……),该智能指针会在当引用次数为 0 时,自动会释放掉指针资源。
策略模式 —— 它不香吗? 策略模式和工厂模式写起来其实区别也不大!策略模式也采用了面向对象的继承和多态机制。 ![]() 在上面工厂模式代码的基础上,按照策略模式的指导思想,我们也来创建一个所谓的策略上下文类,这里命名为RoleContext: class RoleContext {public: RoleContext(RoleOperation *operation) : m_pOperation(operation) { } ! p( C# I$ W4 u$ Q; ?3 V$ m% g ~RoleContext() {; t1 v& A+ i+ d- {2 p' n e3 G3 i; G1 O5 A if (m_pOperation) { delete m_pOperation;0 Q% p& ~" R4 m0 P6 l* k }3 O* N! o, [. S6 z4 u } + }: E3 a6 y/ C! j std::string execute() {. L' u6 I$ l5 g) G! O) s5 G$ W1 X return m_pOperation->Op(); }* y0 n$ |! Y) I" k/ z/ k. f. t private: // 禁止外部拷贝和赋值操作/ {# c. V) ?! p9 x( M: P RoleContext(const RoleContext &); const RoleContext &operator=(const RoleContext &);+ G. }+ K3 M6 @3 X) }$ d) i* ~9 { 8 Y, r5 t6 F! S RoleOperation *m_pOperation; };! v) e1 y# L/ {1 B) P 很明显上面传入的参数operation就是表示不同的「策略」。我们在业务代码里传入不同的角色,即可得到不同的操作结果: class JudgeRole {public:. ]3 X' T# l: l* z std::string Judge(RoleOperation *pOperation) {6 p) Y/ ~( @* ^( u- Q f# [; Z3 i RoleContext roleContext(pOperation);" c, D% F* Z8 f return roleContext.execute();5 ~7 x1 ^4 f* W) h0 ~ }5 ^/ n4 J p- r# Y# Z }; int main() { JudgeRole judgeRole;; q0 g) B* m& J& F: P8 s8 T + w$ {6 Q* J/ o- K! \ std::cout << judgeRole.Judge(new RootAdminRole("ROLE_ROOT_ADMIN")) << std::endl;9 Q: N* ~ K2 j5 Q std::cout << judgeRole.Judge(new OrderAdminRole("ROLE_ORDER_ADMIN")) << std::endl; std::cout << judgeRole.Judge(new NormalRole("ROLE_NORMAL")) << std::endl;- B+ d: P$ {, C # m7 l+ J R( w+ u( I return 0;3 h8 w' u: e ]/ Y2 P" o } // 策略类模板 // 模板参数 ProductType_t,表示的是基类; j2 d v/ [$ H2 w template <class ProductType_t>' t9 V$ c' r5 k3 ~6 ` class ProductContext { public: ProductContext(ProductType_t *operation) ' F0 M. N& B) a2 I) ? : m_pOperation(operation) {! B# } \' X/ D8 f. X0 M. [: J }; W0 E& P* j6 O# S " m! v1 n+ W, V, Q/ S* F, Y ~ProductContext() {+ I2 L! X4 {+ @+ y! n6 Y if (m_pOperation) {7 i' ]! P/ t7 [0 l$ [# |1 y delete m_pOperation; }0 e" m: A: F4 Z' H1 I' |4 U y }6 p' W2 \5 f1 [7 c std::string execute() { return m_pOperation->Op();1 {) W( |5 \; V+ Y0 _; I$ t } ! y% }/ [% L' Z: ^, U4 @1 j2 _3 m private: // 禁止外部拷贝和赋值操作# d( o; ]: r* t% f# Z7 e ProductContext(const ProductContext &);% Q6 d% r: e _5 I: Y K2 w const ProductContext &operator=(const ProductContext &); 9 j" f, o0 h- O& N- s ProductType_t* m_pOperation; };% S# d' S7 S- V5 R9 Y 使用方式,没太大差别,只需要指定类模板参数是基类(如本例 RoleOperation) 即可: class JudgeRole {public: std::string Judge(RoleOperation *pOperation) { ProductContext<RoleOperation> roleContext(pOperation); return roleContext.execute(); } }; 共勉 C++ 和 Java 语言都是面向对象编程的方式,所以都是可以通过面向对象和多态特性降低代码的耦合性,同时也可使得代码易扩展。所以对于写代码事情,不要着急下手,先思考是否有更简单、更好的方式去实现。 C++ 之父 Bjarne Stroustrup 曾经提及过程序员的三大美德是懒惰、急躁、傲慢,其中之一的懒惰这个品质,就是告知我们要花大力气去思考,避免消耗过多的精力个体力(如敲代码)。 |