装饰器(Decorator)模式Recap

Posted bluishglc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了装饰器(Decorator)模式Recap相关的知识,希望对你有一定的参考价值。

设计模式是一套积累并融合了大量经验与成熟思考的设计心法,每一个程序员在成长道路上都应该始终保持对最佳设计方案的不断追求。设计模式也会常用常新,值得反复研究和应用。本文简单Recap一下装饰器(Decorator)模式。

说句题外话,个人认为讲解设计模式最好的书其实是《Head First Design Patterns》一书,因为这本书列举的问题场景和对应的设计模式是高度匹配的,可以让读者深刻体会到对应模式的设计意图,这是所有讲解设计模式的书最难写也是重要的一点,很多书为了讲解一种模式会“硬凹”出一种需求场景,往往和模式本身的用意相“违和”,发而给读者理解这种模式制造了困难。本文就引用《Head First Design Patterns》一书使用的案例来Recap装饰器(Decorator)模式。

《Head First Design Patterns》一书使用了星巴克咖啡店作为案例,店里售卖不同种类的饮料,每一种饮料都有各自的价格,进而构成如下一个自然的类继承体系:


由于每一种饮料在制作时都可以根据用户需要添加或调整一些配料或处理方法,比如:使用全脂/脱脂牛奶,添加巧克力成为摩卡风味等等,不同的配方和制作方法价格自然也会不同,都需要重写cost方法,针对这种场景,一种直白的设计思路就是:拓展类型系统,为每一种可能的组合设计一个具体类,预期结果可能如下:

但是这一设计的“弊病”是很明显的,那就是:“类型爆炸”,众多的类,大部分功能类似或接近,仅仅因为局部的计算逻辑有所差异,就要创建并维护如些多的类是很不合理的。

另一种直白的设计思路就是将这些添加与调整配料或处理方法以属性方式定义到父类Beverage上,由父类Beverage的cost方法统一计算所有这些“附加调味或工序”的费用,然后各饮料子类在自己的cost方法中先计算好自己本身的价格,然后调用父类的cost得到“附加调味或工序”的费用,然后加在一起返回:


这一方案粗看是没有大问题的,可以应对当前面临的问题,但并不是“最优设计”,在应对未来变化的“弹性”和“兼容性”上是比较脆弱的。举个例子:假设咖啡店未来引入了新的调味或加工工序,例如:燕麦咖啡,榛子咖啡等等,则Beverage类就需要进行相应的修改,不管是属性还cost方法,都要动,此次就能暴露出这一设计方案的“不足之处”了:每次对类族进行拓展时,都要修改现有代码,而不是单纯的添加新代码就可以解决问题,也就是违背的OO设计准则的中“开闭原则”。

此外,延伸一点,在这个设计中,即使不知道或没有意识到“开闭原则”,从最朴素的“OO建模”思想去斟酌一下, 我们也能嗅出一点“Bad Smell”来了,因为像milk,mocha等这些属性根本不应该是Beverage这个基类应该持有的属性,因为不是所有的咖啡都拥有这些属性或特征,美式或意式浓缩咖啡就是例子。其实这个时候,我们这个Beverage类族的OO建模已经开始和现实业务中的实体不符了,这是建模的“失真”,经验告诉我们:只要对象建模与实际业务实体之间存在不恰当的映射关系,或早或晚都会出现让人感到“别扭”的地方,表现可能是:总是需要频繁的修改代码,或修改一处代码会牵连更多的代码同步修改和测试等等。

接下来,就是最优设计:装饰器(Decorator)模式的解决方案了:

整体设计思路一目了然:把那些“附加调味或工序”抽离到独立的子类体系中,这串子类统称为“CondimentDecorator”,这些类本身继承自Beverage,也就是说,它们性质上,也是一种独立的Beverage,但其构造函数必须以一个现成的Beverage的具体类作为参数,那就意味着它们不能独立初始化,而必须要以一个基础Beverage实例(如Espresso)作为包裹对象,在叠加自己的计费逻辑进去。

这一改动,最大的收益就是彻底规避了后续拓展时改动现有代码的风险,当有新燕麦或榛子咖啡上线时,只须在CondimentDecorator下添加相应的子类并实现先关逻辑即可,是对开闭原则非常完美的一次诠释。

以上是关于装饰器(Decorator)模式Recap的主要内容,如果未能解决你的问题,请参考以下文章

设计模式:装饰器(Decorator)模式

装饰器模式--Decorator

装饰器模式-Decorator(Java实现)

设计模式入门之装饰器模式Decorator

Decorator(装饰器模式)

设计模式——装饰器模式(Decorator Pattern)