第一场 难题未解
布景:铁岭,晴天,午后,风。在一幢还算气派的写字楼的三层外墙上,挂着一条红色横幅,上面用歪歪扭扭的毛笔字写着“东北F4软件外包工作室”。大风中,那早已褪色的条幅剧烈地抖动着,发出阵阵嘶吼。房间内,东北F4正在为大鹏科技股份有限公司开发一款“大侠”游戏。
刘能(坐在椅子上,扭头冲众人):好……好啦,搞……搞定!我创建了一个大侠虚基类,然后派生出了三峡和张凤霞两……两个实例,每个大侠有两个功能:攻击和隐……隐身。这是我画的那什么U……U什么图:
刘能:还有,这是我写的代……代码:
1 class DaXia //虚基类 2 { 3 public: 4 DaXia() {}; 5 virtual ~DaXia() {}; 6 virtual void GongJi() = 0; 7 virtual void YinShen() = 0; 8 }; 9 10 class SanXia :public DaXia //三峡版本 11 { 12 public: 13 SanXia() {}; 14 virtual ~SanXia() {}; 15 virtual void GongJi() 16 { 17 cout << "来自三峡的攻击!" << '\n'; 18 } 19 virtual void YinShen() 20 { 21 cout << "三峡已隐身!" << '\n'; 22 } 23 }; 24 25 class ZhangFengXia :public DaXia //张凤霞版本 26 { 27 public: 28 ZhangFengXia() {}; 29 virtual ~ZhangFengXia() {}; 30 virtual void GongJi() 31 { 32 cout << "来自张凤霞的攻击!" << '\n'; 33 } 34 virtual void YinShen() 35 { 36 cout << "张凤霞已隐身!" << '\n'; 37 } 38 };
(众人围拢过来看)
宋小宝:那啥,刚才大鹏给我打电话了,说每个大侠除了攻击和隐身两个核心功能外,还可能会有一些小的辅助功能,比如在攻击之前先跳一下,或者在隐身之前先喊话。之所以说可能,是因为对特定的大侠而言,可能完全具备这些辅助功能,也可能只具备一部分,还可能一点都不具备。总之,就是不确定,你们看,这咋整?
赵四(一脸得意):有什么不好整的,太简单了,让我来!
(两分钟后……)
赵四(愈发得意,将一张纸拍在众人面前):看吧!简直完美!
赵四(摇头晃屁股):我设计的新类聚合了DaXia类,每个新类实现自己的动作,而原有的动作复用所聚合的类的代码。由于聚合的是大侠类的基类DaXia,所以所有的具体大侠类都可以被新类聚合,即新类适用于每一个具体大侠类,这在一定程度上减少了新增类的数量。假设我们需要一个在攻击前跳跃的三峡,只要用TiaoDaXia类的实例聚合一个SanXia类的实例就可以了;再比如,假设我们需要一个在攻击前跳跃,在隐身前喊话的张凤霞,只要用一个TiaoShuoDaXia类的实例聚合一个ZhangFengXia类的实例就可以了。你们说,我的设计是不是很棒……
小沈阳(没等赵四说完,就一巴掌扇到他的左脸上):棒你个脑袋!你设计的是啥玩意!你设计的新张凤霞类根本就不是大侠类的子类,这在逻辑上本身就不合理!更重要的,你看看我客户端的接口,是这样式儿的:
void DaXiaDongZuo(DaXia *daxia) { daxia->GongJi(); daxia->YinShen(); }
看着没?你说你设计的破玩意,叫我怎么用?我得改代码,得改,知道不!要是明儿需求有了新的变化,照你那么设计,我还得改,还得改,知道不!(伸手指着赵四)你软件开发是体育老师教的啊!知道啥叫“开闭原则”不?面向扩展开放,面向修改关闭!
赵四(一手捂着脸,一手指着小沈阳,愤怒而胆怯):你干嘛打我!不好就不好吧,你凭什么打我!(作发狠状)小样,这事没完,没完,知道不!今天这事必须有个了结!有种你给我等着,你给我十分钟,十分钟后,我保证——保证给你一个满意的方案!
(十分钟后)
赵四(战战兢兢走到小沈阳面前,颤抖着把一张纸拍在桌子上,马上又往后跳了一大截):小样,看好了!这回你满意了吧!这回你服了吧!你说,服不服!
(众人谁都没有理会赵四,全凑过去看赵四的新方案)
赵四(神气活现地):这回服了吧!我采用派生子类的方式,完美地解决了你刚才说的所有问题。怎么样,我是不是很机智……
刘能(还没等赵四说完,一巴掌扇在他的右脸上):机智你个脑袋!他的问题解决了,我的呢?现在需要一个既跳又说的张凤霞,我就得新派生一个类,接下来,只跳不说的一个,只说不跳的一个,这就是三个;三峡下边,又是三个,这就六个了。如果明天还有新的需求出现,我还不得累吐血了啊!你来编码,你试试!这么多类,你叫我怎么维护?再说,你这是最简单,也是最笨、最愚蠢的做法!你发现了吗,对于跳这个功能,大家都是一样的,而你的设计,却不得不每个类都写一遍!一点复用都没有!
赵四(哭丧着脸走向宋小宝):宝啊,他们都欺负我,你说,要扩展一个类的方法,不就是聚合和继承吗?可他们……呜……宝啊……(作拥抱状)
宋小宝(一把推开赵四,极其轻蔑地)瞧你那损色!净想些找抽的方法!该!
赵四(委屈地):没想到连你也欺负我!(不服地)你还说我呢,有本事你想一个好办法!
宋小宝(心虚地):我要是有……有办法,还轮得着你挨打吗?
小沈阳(不耐烦地):行了行了,到下班时间了,今天先这样吧。大家各回各家,各找各妈。回去都想想,看这事咋整。
众人(无奈地):好吧,也只有如此了。
(众人下,传来赵四的嘟囔声:你们都给我听好了,这事没完,我的打不会白挨,明儿,我一定叫你们服服帖帖的……)
第二场 赵四逆袭
布景:打了一夜麻将的宋小宝揉着惺忪的睡眼晃晃悠悠走进工作室,见其他三个人正围在一起说着什么……
宋小宝(疑惑地):咋地啦,啥事呀?
赵四(神气活现、摇头晃脑,提着一张纸走到宋小宝面前):宝啊,看着没?哥夜以继日、通宵达旦、彻夜未眠、左思右想、绞尽脑汁、搜肠刮肚,终于在黎明的曙光中想出了完美的解决方案!怎么样,服不服?
宋小宝(仰起头发出一阵魔性的笑声):哈哈哈哈哈哈……这一大早上班,就听到这么好笑的笑话,(转向赵四,轻蔑地)瞧你那损色!你要是把这事摆平了,我就请大伙吃饭……
刘能、小沈阳(挥拳作胜利状):耶!
宋小宝(瞬间蒙圈):不是……咋……咋地?(疑惑地望着刘能和小沈阳,指着赵四)他真想出来了?
刘能、小沈阳(冲宋小宝肯定地点头):这回是真的!
宋小宝(心虚而疑惑地):等……等会儿,让我好好看看。
(众人的目光又聚集到赵四的设计上)
(好几分钟后)
宋小宝(疑惑地望着赵四):你这设计的什么玩意啊,跟天书似得!
赵四(撇嘴):瞧你那损色!这都看不懂!
宋小宝(厉声):别抢我台词!有本事,你给解释一下。
(赵四用期待的目光望向刘能和小沈阳)
刘能(假装没看见,催促道):行啦,赶紧说吧,宝儿都等不及了。
小沈阳:大家鼓掌!
(众人鼓掌)
赵四(故作姿态地清了清嗓子):嗯嗯!我们看,在我们设计的游戏中,攻击和隐身是主要的、核心的功能,而攻击前跳跃、隐身前喊话是依托核心功能而存在的、是对核心功能的扩展和补充。可以说,跳跃和喊话是对核心功能的一种装饰。
众人(点头):有道理!
赵四(大模大样地徘徊在众人中间):所以,我从大侠类派生出一个装饰大侠(ZhuangShiDaXia)类,专门处理经过装饰后的大侠的动作。
刘能(举手打断):等一下!这不是和昨天一样吗?还得写好多好多类。
赵四(伸出右手食指在众人面前摇晃,同时缓慢地摇头):No、No、No,非也非也。大家仔细看,我把每种装饰都作为ZhuangShiDaXia类的子类。例如,在我们的问题中,有跳跃和喊话两种装饰,于是我从ZhuangShiDaXia类派生出跳大侠(TiaoDaXia)和说大侠(ShuoDaXia)两个子类,我们不妨称之为具体装饰类。每个具体装饰类都只实现自己的装饰功能,而其它功能则采用daxia指向的对象的版本。以TiaoDaXia为例,在它的GongJi功能中,先调用自己的Tiao功能,而具体的GongJi功能和该装饰无关的核心功能,如YinShen功能,则调用daxia指向的对象的版本。
刘能(不解地):daxia又是个什么鬼?
赵四(神秘而庄重地):然后就是重点了。不知大家是否注意到,ZhuangShiDaXia类除了派生自DaXia类,还聚合了DaXia类,即ZhuangShiDaXia类中有一个DaXia*类型的指针daxia。再回过头来看,由于ZhuangShiDaXia类派生自DaXia类,所以,daxia可以指向上图中的所有类,包括ZhuangShiDaXia类的子类。
小沈阳(一脸迷惑):这很重要吗?
赵四(激动地):太重要了!以ZhuangShiDaXia类的子类TiaoDaXia类为例,很显然,它继承了ZhuangShiDaXia类的daxia指针,而这个指针,还可以指向ZhuangShiDaXia类的子类,比如ShuoDaXia类,而ShuoDaXia类的daxia指针又可以指向其它DaXia类的子类……这样,就可以实现类的“层次嵌套”,也可以理解为一种递归,于是,就可以实现功能的自由组合和调用时的“委托”。换句话说,你想要什么样的大侠,就可以组合出什么样的大侠,简直是变化万千,无穷无尽!哎呀,不能再说了,再说下去,我都佩服死自己了,哈哈哈哈!
宋小宝(迷惑地):好像明白了,又好像不明白,能举个例子吗?
赵四(胸有成竹地):当然能啦,我已经写好了一个Demo,大家请上眼!
1 #include2 3 using namespace std; 4 5 class DaXia //抽象基类 6 { 7 public: 8 DaXia() {}; 9 virtual ~DaXia() 10 {}; 11 virtual void GongJi() = 0; 12 virtual void YinShen() = 0; 13 }; 14 15 class SanXia :public DaXia //三峡类 16 { 17 public: 18 SanXia() {}; 19 virtual ~SanXia() 20 {}; 21 virtual void GongJi() 22 { 23 cout << "来自三峡的攻击!" << '\n'; 24 } 25 virtual void YinShen() 26 { 27 cout << "三峡已隐身!" << '\n'; 28 } 29 }; 30 31 class ZhangFengXia :public DaXia //张凤霞类 32 { 33 public: 34 ZhangFengXia() {}; 35 virtual ~ZhangFengXia() 36 {}; 37 virtual void GongJi() 38 { 39 cout << "来自张凤霞的攻击!" << '\n'; 40 } 41 virtual void YinShen() 42 { 43 cout << "张凤霞已隐身!" << '\n'; 44 } 45 }; 46 47 class ZhuangShiDaXia :public DaXia //装饰大侠类 48 { 49 protected: 50 DaXia *daxia; //聚合DaXia类的直接或间接子类 51 public: 52 ZhuangShiDaXia() {}; 53 ZhuangShiDaXia(DaXia *dx) 54 :daxia(dx) 55 {} 56 virtual ~ZhuangShiDaXia() 57 { 58 delete daxia; 59 daxia = NULL; 60 } 61 virtual void GongJi() //由所聚合的类实现功能 62 { 63 daxia->GongJi(); 64 } 65 virtual void YinShen() 66 { 67 daxia->YinShen(); 68 } 69 }; 70 71 class TiaoDaXia :public ZhuangShiDaXia //具有跳装饰的大侠类 72 { 73 protected: 74 virtual void Tiao() //实现跳装饰 75 { 76 cout << "跳一下" << '\n'; 77 } 78 public: 79 TiaoDaXia() {}; 80 TiaoDaXia(DaXia *dx) 81 :ZhuangShiDaXia(dx) 82 {} 83 virtual ~TiaoDaXia() 84 {} 85 virtual void GongJi() 86 { 87 Tiao(); 88 daxia->GongJi(); //具体的攻击功能交由所聚合的类完成 89 } 90 91 //YinShen功能完全采用基类版本,即daxia->YinShen(),还是交由所聚合的类完成 92 }; 93 94 class ShuoDaXia :public ZhuangShiDaXia //具有说装饰的大侠 95 { 96 protected: 97 virtual void Shuo() //实现说装饰 98 { 99 cout << "你来找我呀?!" << '\n'; 100 } 101 public: 102 ShuoDaXia() {}; 103 ShuoDaXia(DaXia *dx) 104 :ZhuangShiDaXia(dx) 105 {} 106 virtual ~ShuoDaXia() 107 {} 108 virtual void YinShen() 109 { 110 Shuo(); 111 daxia->YinShen(); //具体的隐身功能交由所聚合的类完成 112 } 113 114 //GongJi功能完全采用基类版本,即daxia->GongJi(),还是交由所聚合的类完成 115 }; 116 117 int main() 118 { 119 ZhangFengXia *zfx = new ZhangFengXia(); //张凤霞 120 ShuoDaXia *sdx = new ShuoDaXia(zfx); //将张凤霞“嵌入”到具有说装饰的大侠实例中,构造出具有说装饰的张凤霞 121 TiaoDaXia *tdx = new TiaoDaXia(sdx);//将具有说装饰的张凤霞“嵌入”到具有跳装饰的大侠实例中,使得最终的大侠具有跳装饰和说装饰 122 tdx->GongJi();//攻击 123 tdx->YinShen();//隐身 124 delete tdx; 125 tdx = NULL; 126 sdx = NULL; 127 zfx = NULL; 128 129 return 0; 130 }
赵四(洋洋得意地):来,走一波!(赵四运行了程序)
赵四(自豪地):看到了吧,我们构造出的大侠具有了跳装饰和说装饰。
刘能(懵懂地):还是不太懂……
赵四(故作不耐烦):好吧,我就再启发你一下。(又拿出一张纸)
赵四:如上图所示,ZhangFengXia实例嵌入到ShuoDaXia实例中(即ShuoDaXia的daxia成员指向ZhuangFengXia实例),ShuoDaXia实例嵌入到TiaoDaXia实例中。当执行tdx->GongJi()时,执行代码
virtual void GongJi() { Tiao(); daxia->GongJi(); //具体的攻击功能交由所聚合的类完成 }
即先调用Tiao(),输出“跳一下”,然后执行daxia指向的实例的GongJi()函数,而此时的daxia(即this->daxia)指向的是ShuoDaXia实例,而ShuoDaXia类中的GongJi()函数依然采用的是从它的基类ZhuangShiDaXia继承过来的GongJi()函数,即依然执行daxia->GongJi(),而此时的daxia指向ZhangFengXia实例,于是调用ZhangFengXia类的GongJi()函数,输出“来自张凤霞的攻击!”。
tdx->YinShen()的执行类似。先执行TiaoDaXia类的YinShen()函数,而该函数完全继承自基类ZhuangShiDaXia,于是执行
virtual void YinShen() { daxia->YinShen(); }
而此时daxia指向ShuoDaXia实例,于是调用ShuoDaXia实例的YinShen()函数,即
virtual void YinShen() { Shuo(); daxia->YinShen(); //具体的隐身功能交由所聚合的类完成 }
于是先调用Shuo(),输出“你来找我呀?!”,然后再调用daxia指向的实例的YinShen()函数,而此时的daxia指向ZhangFengXia实例,于是调用ZhangFengXia类的YinShen()函数,输出“张凤霞已隐身!”。
众人(恍然大悟):哦,明白了!
小沈阳(若有所思):也就是说,具体装饰类只具体实现自己的装饰部分,而其它的,都交由它所聚合的类完成。例如,对TiaoDaXia而言,它只实现具体的跳功能,并将其封装进自己版本的GongJi()函数中,而具体的攻击行为和不由它装饰的隐身行为,则统统采用它所聚合的类的版本。
宋小宝(抢着道):更为关键的是,如果被聚合者也同样含有daxia指针,即是ZhuangShiDaXia的子类的话,它还会聚合其它的类,直到被聚合者不含daxia指针,如SanXia类和ZhangFengXia类。对于赵四给出的Demo,可以抽象出这样的“类的嵌套模型”。(举起手中的一张纸)
刘能:没……没错!(指着赵四画的UML类图)通过继承和聚合,DaXia类和ZhuangShiDaXia类构成一个环,于是就可以实现类的“递归嵌套”了,而“递归出口”就是不含daxia指针的类。
小沈阳:而这种“递归嵌套”的过程,就是功能组合的过程。分析赵四给出的Demo,不难看出,从代码的层面来看,嵌套,或者说功能组合是由内而外的,而调用是由外而内的。如果调用到的方法是具体装饰类自己的版本,则先执行相关的装饰功能,然后将核心功能交给“下一层”,这样“层层委托”,直到未经装饰的类。
刘能(激动地):我彻……彻底懂了,我还能举……举一反反……三呢,看,如果想要一个在攻击之前跳两次的大侠,这……这么写就可……可以了。(说着敲出如下代码)
ZhangFengXia *zfx = new ZhangFengXia(); //张凤霞 TiaoDaXia *tdx1 = new TiaoDaXia(zfx); //跳一下 TiaoDaXia *tdx = new TiaoDaXia(tdx1); //再跳一下
赵四(摸着刘能的头):孺子可教也!
刘能(扒拉开赵四的手):边去!别摸我头!
赵四(故作高深地):其实,还有更简洁的写法。(敲出如下代码)
TiaoDaXia *tdx = new TiaoDaXia(new TiaoDaXia(new ZhangFengXia()));
小沈阳(若有所思):你别说,赵四整的这个“装饰模式”,还挺好的。我们知道,当需要对一个类的功能进行扩展时,一般有聚合和继承两种方式,前者可以减少新增加的类的数量,但可能会带来与客户端接口不兼容的问题,需要修改客户端;后者虽然保持了接口的一致性,但在变化有多种组合时,子类数量激增。装饰模式同时采用继承和聚合,两者相辅相成、相生相克,既保持了各自的优点,又克服了对方的缺点,更重要的是,由于同时使用了继承和聚合,可以动态地实现类的层次嵌套和功能的自由组合,非常的灵活。
刘能:没……没错!当我们需要对核心功能进行装饰,而这些装饰又有很多变化和组合的时候,采用装饰模式是极好的。而且,当增加新的变化时,完全不用修改当前的代码,只需要增加一个具体装饰类就可以。也就是说,我们可以很方便地、随时随地地扩展功能,简直了,还有谁?very good!
宋小宝(不屑地撇嘴):得了,消停会儿吧,又不是你发明的!
赵四(故作深沉地):任何事物都有两面性。拿装饰模式来说,刚才大家都说了它的优点,却忽略了它的不足。与单纯的继承相比,装饰模式减少了项目中类的数量,但在具体应用时,却增加了客户端创建的对象的数量。假设我们需要一个在攻击前跳一下,在隐身前喊话的张凤霞,如果采用继承的方法,我们会从ZhangFengXia类派生一个具有相应功能的NewZhangFengXia类,客户端只需要创建一个NewZhangFengXia类的实例就可以了;而如果采用装饰模式,正如大家在我写的Demo里看到的,由于我们要组合、拼凑出相应的大侠,所以需要创建ZhangFengXia类的实例、ShuoDaXia类的实例、TiaoDaXia类的实例,共计3个实例。如果我们的类很大很复杂,那么,创建类的实例是比较耗时的,这样会影响系统效率。另外,装饰模式中对象之间是层层嵌套的,这就使得最终组合出来的对象比较复杂,一个调用会引起由外而内的一连串调用,一旦出现问题,我们往往不知道问题具体出现在哪个环节,只能逐层排查,显然这是很麻烦的。再者,正如大家看到的,在装饰模式下,客户端构造大侠实例的代码的可读性比较差,对于一个不懂设计模式的人来说,是比较难看懂这些代码的。所以,总的来说,在一定程度上,装饰模式会降低项目的效率,增加项目的复杂度。
小沈阳:可以呀,赵四,没看出来啊!有两把刷子!
(响起深沉伤感的音乐)
赵四(缓慢而深沉地):谢谢!(泪眼朦胧地眺望着远方)记得那是2010年,家里拿钱让我上《非诚勿扰》。我刚一上场,24盏灯就呼啦全灭了。从此,我就开始了屡败屡战的相亲征途。直到第999次相亲失败,我总算醒悟了,就我这副尊容,去相亲,说白了就是浪费感情。万念俱灰之下,我拿相亲剩下的钱,买了一台二手电脑,从此踏上了程序员这条不归路。我不像你们,都是加里敦、地理赛这些名牌大学的高材生,我是半路出家。于是,我是时时受欺负、处处被鄙视,可是(使劲挥拳),我不甘心!虽然我还没有媳妇,但我也是个男人!我也有尊严!所以,我一定要用实力证明自己!于是,我千方百计、处心积虑、不择手段地学习!终于,皇天不负苦心人,我做到了!
小沈阳(揉着眼睛,呜咽着说):太感人了!四儿呀,对不起,以前是大哥不好,以后,大哥再也不欺负你了!
刘能(擤了把鼻涕):太……太励志了!四儿呀,对……对不起,以前是二……二哥不好,以后,二哥再也不鄙……鄙视你了!
(宋小宝悄悄向门口退去,却不小心碰倒了垃圾桶,于是被小沈阳和刘能发觉)
小沈阳、刘能(追着跑在前面的宋小宝):宋小宝,你给我站住!
(完)