装饰者模式

Posted ZhangJianIsAStark

tags:

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

这篇博客记录一下装饰者模式。


我们首先借用一下Head First中的例子,来看看装饰者模式涉及的应用场景。

假设我们需要开发一个饮料计费系统,如下图所示。

Beverage作为所有饮料的父类(抽象类或接口均可),
定义了一个cost方法,用于计算饮料的价格。

起初定义了四种基本的饮料,HouseBlend、DarkRoast、Decaf和Espresso。
这些饮料均继承Beverage,并实现各自的cost方法。


在上文的场景下,假设客户在购买饮料时,
可以选择性地向基本饮料中加入不同的调料。
基本饮料加入调料后,就变成了一种“新”的饮料,
需要重新实现cost方法。

如果我们利用继承的方式,实现这些“新”的饮料,
那么整个计费系统的类图将变成如下的结构:

如上图所示,我们仅添加了两种调料milk和tea,
但整个设计体系中立马新增了许多子类。
容易预见,随着基本饮料和调料种类的增加,
这些子类的数量会进一步增多,达到一个无法维护的数量。

试想一下,如果某个基本饮料或调料的价格发生改变,
那么就有许多类涉及的代码需要调整,维护这样一种代码,
无疑是程序员的噩梦。


针对这种问题,有的朋友可能会这么解决:

在父类Beverage中增加标志位,来表示是否添加了某种调料。
同时,增加对应的设置和判断接口。
这么一来,子类在计算价格时,就可以通过父类的接口,
判断是否添加了某种饮料,然后根据判断结果来计算价格。

通过这种方式,就可以大量地减少子类的数量,
整个设计结构清晰易懂。

然而,这么设计也有一个致命的缺陷。
如果新增了调料的种类,那么每个子类的cost方法还是需要重写。
同时,随着调料种类的增加,子类的cost方法中,
必然存在大量用于判断是否含有某种饮料的判断语句。

从整体来看,这种设计方式还是不够优雅,
违背了对扩展开发,对修改关闭的设计原则。


为了解决这类问题,就需要使用本篇博客的主角装饰者模式了。

整个装饰者的设计思路基本上可以用下图表示:

如上图所示,假设我们需要一个加了Milk和Tea的HouseBlend。
那么我们可以建立Milk和Tea的类,继承自Beverage。

在代码运行时,我们可以动态地用Tea来包装HouseBlend,得到的一个Beverage对象;
然后,继续用Milk来包装这个新的Beverage对象,得到最终的Beverage。
此时,Milk和Tea类就可以看作HouseBlend的装饰者对象。

在计算整体的价格时,我们可以直接调用最终的Beverage的cost接口。
我们已经知道,最外层的实际上是个装饰者对象。
于是,装饰者对象会进一步调用其持有的Beverage对象的cost接口。
如果下一个Beverage对象,仍然是个装饰者,
那么它会进一步调用其持有的Beverage对象的cost接口。

通过如图所示的递归调用,最后将调用到基本饮料的cost接口,
得到基本饮料的价格。
然后,在基本饮料价格的基础上,
逐步增加调料本身的价格,就可以得到最终的价格。

通过这种方式,不论调料如何改变,我们都容易写出清晰简单的代码。


现在,是时候来看看装饰者模式的定义和结构图了。

装饰者模式动态地将责任附加到对象上。
若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

上图的Component是被装饰对象的父类,
ConcreteComponent是实际的被装饰对象。

Decorator是装饰对象的父类或共同接口;
ConcreteDecoratorA和ConcreteDecoratorB是实际的装饰者对象,
这些对象将持有Component对象的引用,同时作为Component的子类。

对应装饰者模式的结构图,我们看看上述场景改良后的设计结构:

对比上文的装饰者模式结构图,新的设计应该是比较容易理解的。


在本文的最后,我们看看装饰者模式的一个实际应用场景,
Java IO中输入流设计结构:

在了解装饰者模式后,再看下面的代码,是不是容易理解的多:

.............
InputStream in = new BufferedInputStream (
        new FileInputStream("test.txt"));
.............

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

Java设计模式之装饰者模式

设计模式整理_装饰者模式

设计模式-装饰者模式(Go语言描述)

设计模式-装饰者模式(Go语言描述)

装饰者模式

设计模式 之 装饰者模式