欲戴皇冠,必承其重
——莎士比亚《亨利四世》
动态地给一个对象添加一些额外的职责。就添加功能来说,装饰者模式比生成子类更为灵活(该模式也是继承关系的替代方案之一)
想必各位道友一定喝过饮料吧?
无论是雪碧、芬达,冰红茶,还是快乐水,都不是直接放在玻璃瓶里售卖的。而是会放在一个包裹得花里胡哨得饮料瓶里售卖。但是这些用来包装饮料瓶得包装纸上的内容,既风格迥异,又有相通的地方。如果要我们为这样的一个东西设计类库,他应该是什么样的呢?
准备好了?这次的例子也开始了:
假定现在有 饮料制造商 A ,A旗下有 X/Y/Z 三种饮料,且每种饮料都有原味、草莓和芒果三种口味,就像这样:
很显然,我们一定会有一个关于 Delineator(绘图器)
的类树,用来定义 X、Y和Z 的包装纸的基础样式,就像这样:
//绘图器接口
public interface Delineator{
//具体绘制方法
void draw();
}
public class Delineator_X implements Delineator{
public void draw(){
System.out.println("饮料X的绘制方案");
}
}
public class Delineator_Y implements Delineator{
public void draw(){
System.out.println("饮料Y的绘制方案");
}
}
public class Delineator_Z implements Delineator{
public void draw(){
System.out.println("饮料Z的绘制方案");
}
}
这个结构毋庸置疑,但是问题在于,要怎么把 草莓 和 芒果 跟他们组合起来呢?
继承又被我们搬出来了,很容易就可以想到他的解决方式,就是这样:
虽然是错误示范,但是画的时候还是觉得好蠢
这个问题在前几篇文章中已经聊过很多次了,诸如 Delineator_X_Strawberry 这样的子类,并不是和其他子类有本质上的不同,这些子类之间的区别,无非就是对相同的数据进行排列组合而已
也就是说,如果我新增一个 葡萄味 的新口味,或者增加一个 W类型 的新饮料,那么这颗 绘图器类树 上的子类数量将会成倍增长
类爆炸 又出现了,这显然是一个非蠢即懒的解决方案
沿着之前的思路,既然继承在这里是在 排列组合,那我干脆就组合他如何,就像这样:
//绘图器抽象类
public abstract class Delineator{
private SessoningDelineator sessoningDelineator;
//getter && setter 略
//具体绘制方法
public abstract void draw();
}
public class Delineator_X extends Delineator{
public void draw(){
getSessoningDelineator().changeColor();//改变颜色
System.out.println("饮料X的绘制方案");
getSessoningDelineator().drawIcon();//绘制图标
}
}
public class Delineator_Y extends Delineator{
public void draw(){
getSessoningDelineator().changeColor();//改变颜色
System.out.println("饮料Y的绘制方案");
getSessoningDelineator().drawIcon();//绘制图标
}
}
public class Delineator_Z extends Delineator{
public void draw(){
getSessoningDelineator().changeColor();//改变颜色
System.out.println("饮料Z的绘制方案");
getSessoningDelineator().drawIcon();//绘制图标
}
}
//佐料绘制器
public interface SessoningDelineator{
//修改颜色
void changeColor();
//绘制图标
void drawIcon();
}
public class StrawberrySeasoningDelineator implements SessoningDelineator{
public void changeColor(){
System.our.println("修改画笔颜色为红色");
}
public void drawIcon(){
System.our.println("左下角绘制草莓");
}
}
public class MangoSeasoningDelineator implements SessoningDelineator{
public void changeColor(){
System.our.println("修改画笔颜色为黄色");
}
public void drawIcon(){
System.our.println("右下角绘制芒果");
}
}
有一个好消息和一个坏消息
好消息是这种方案可以实现我们现阶段的需求,似乎就是我们要的答案
坏消息是仅仅只是似乎
我们通过定义 SessoningDelineator(佐料绘图器)
类簇的方式,实现了口味和饮品之间的解耦。也实现了在程序运行的过程中动态组合口味和饮品的功能
这种看起来像 策略模式 的解决方案,确实像策略模式一样实现了通过切换算法簇,从而动态改变接口行为的目的
但是他的解耦度还不够,有两个大的问题
SessoningDelineator 内的方法过于死板,只是在目前形势下可以完美契合需求
比如说我现在新增 葡萄味 的饮品,而且规定 葡萄味 的包装纸是使用艺术字体进行打印。那你就只能在 SessoningDelineator 上新增一个类似 changeFont 这样的方法来切换字体
但是这个方法对 草莓 和 芒果 来说是毫无意义的,你的改变影响了他们
没有人规定口味只能有一种
参考 农夫果园,有单种、两种甚至N种蔬果组合而成的风味
那你会说,那我可以把单个 SessoningDelineator 对象,改成维护一个 SessoningDelineator 列表呀,然后在 draw 中循环调用他们不就可以了
这样也不行,这样会导致你写入列表的顺序至关重要,他将决定风味的渲染顺序
所以这种方案只能解一时之需,我们还需要可以持续化发展的方法
如果用装饰者解决这个问题,那么他会是这样的:
//绘图器接口
public interface Delineator{
//具体绘制方法
void draw();
}
public class Delineator_X implements Delineator{
public void draw(){
System.out.println("饮料X的绘制方案");
}
}
public class Delineator_Y implements Delineator{
public void draw(){
System.out.println("饮料Y的绘制方案");
}
}
public class Delineator_Z implements Delineator{
public void draw(){
System.out.println("饮料Z的绘制方案");
}
}
public abstract class DelineatorDecorator implements Delineator{
private Delineator delineator;
//getter & setter 略
public DelineatorDecorator(Delineator delineator){
this.delineator = delineator;
}
public void draw(){
beforeDraw();
delineator.draw();
afterDraw();
}
public abstract void beforeDraw();//绘制之前
public abstract void afterDraw();//绘制之后
}
public class StrawberryDelineatorDecorator extends DelineatorDecorator{
public StrawberryDelineatorDecorator(Delineator delineator){
super(delineator);
}
public void beforeDraw(){
System.our.println("修改画笔颜色为红色");
}
public void afterDraw(){
System.our.println("左下角绘制草莓");
}
}
public class MangoDelineatorDecorator extends DelineatorDecorator{
public MangoDelineatorDecorator(Delineator delineator){
super(delineator);
}
public void beforeDraw(){
System.our.println("修改画笔颜色为黄色");
}
public void afterDraw(){
System.our.println("右下角绘制芒果");
}
}
我们取消了 SessoningDelineator,把两个类簇结合成了一个类簇。风味相关的代码,我们把他们整合到 DelineatorDecorator(绘图装饰器)
中,而且由于 DelineatorDecorator 也是 Delineator 的子类,所以你可以像调用 DelineatorDecorator_X 的对象一样,调用 DelineatorDecorator 的对象
当你需要一个 草莓味的Y类型 饮品包装纸绘制器时,你只需要这样做:
new StrawberryDelineatorDecorator(new Delineator_Y());
这样一来,上一个例子中的第二个问题迎刃而解,当我们需要多种风味的时候,无非就是多写个new而已
上一个例子中的第一个问题就更简单了,没有人规定 DelineatorDecorator 中只能描述和风味有关的绘制信息,不管是修改字体,还是修改纸张,甚至增加别的国家的QS标志,都是没问题的
这种组装对象的方式,给了我们极大的自由度
我们自始至终都没有改变最基础的 X/Y/Z 的绘制方法,但是却通过环绕调用 X/Y/Z 的绘制方法的方式,的的确确改变了他们最终的成品
而这正是一个标准的适配器模式实现
既然装饰者是继承关系的替代方案之一,那为什么在装饰者的实现中要使用继承呢?
答:继承有两个作用:
行为
类型
装饰者模式中使用继承是为了后者,为了让 client 在使用装饰者的时候可以直接使用装饰者类创建出来的对象,为了让装饰者可以融入主体的类簇中
通常我们定制一个对象,除了改变状态的方式之外,只能通过定义子类
装饰者让我们可以动态的去定制一个对象,而且不需要影响已有的类簇,这是很牛的一个方案
但是由于装饰者的拓展方向太多,当你需要定制的内容越来越多,你的装饰者子类就会越来越多,每一个小功能都意味着需要创建一个新的装饰者子类。这不算类爆炸,但也不是什么值得夸耀的事情
Java io流,这是特别经典的装饰者模式实现
你觉得Java的io流类库设计得如何呢?
事实上褒贬不一
他确实给我们提供了巨大的自由度,我们甚至可以自定义io装饰器,修改io流的行为。但这也导致我们每次用io流,基本都是操作复数对象,从执行动作后的close就可以看出来有多麻烦
还是那句话,要学会在合适的环境下使用合适的方法,再牛的手法都拯救不了不会变通的程序员
相信你也发现了,装饰者虽然避免了排列组合子类的情况的发生,但是由于每个装饰者类都不在一个体系中,所以每次使用装饰者,都意味着大量的对象在同一时间被创建,这也算是装饰者的缺陷之一
你应该没见过麦当劳宣传自己用了什么牌子的生菜,因为重要的永远是夹在里面的肉
但这不代表包裹肉的面包和香菜就没有存在的价值,你能说鸡肉堡和鸡肉卷的味道是一样的吗?但是肉是一样的肉,他们的区别就是装饰在他们外层的内容所体现的
在世界这个大舞台上,绝大多数人都只是龙套而已。可是哪怕是电影里出境时间不到一秒的路人,都可以根据他的生平来写一部外传。所以哪怕是配角,是龙套,是装饰者,对他自己的故事而言,舞台中央的那个人,才是配角
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容