设计模式——1_3 装饰者(Decorator)

欲戴皇冠,必承其重
——莎士比亚《亨利四世》

文章目录

  • 定义
  • 图纸
  • 一个例子:定制饮料瓶包装纸的打印规则
    • 第一种解决方案
    • 第二种解决方案
    • 装饰者解决方案
  • 碎碎念
    • 装饰者和继承
    • 装饰者和定制对象
    • 装饰者和大量对象
    • 装饰者和汉堡包

定义

动态地给一个对象添加一些额外的职责。就添加功能来说,装饰者模式比生成子类更为灵活(该模式也是继承关系的替代方案之一)


图纸

设计模式——1_3 装饰者(Decorator)_第1张图片


一个例子:定制饮料瓶包装纸的打印规则

想必各位道友一定喝过饮料吧?

无论是雪碧、芬达,冰红茶,还是快乐水,都不是直接放在玻璃瓶里售卖的。而是会放在一个包裹得花里胡哨得饮料瓶里售卖。但是这些用来包装饮料瓶得包装纸上的内容,既风格迥异,又有相通的地方。如果要我们为这样的一个东西设计类库,他应该是什么样的呢?

准备好了?这次的例子也开始了:


假定现在有 饮料制造商 A ,A旗下有 X/Y/Z 三种饮料,且每种饮料都有原味草莓芒果三种口味,就像这样:

设计模式——1_3 装饰者(Decorator)_第2张图片

  • X、Y和Z 三种类型的饮料都有各自不同的饮料瓶包装纸
  • 原味 必须要用黑颜色印刷字体
  • 草莓味 必须要用红颜色印刷字体,并在印刷完毕后在左下角添加一个草莓图案
  • 芒果味 必须要用黄颜色印刷字体,并在印刷完毕后在右下角添加一个芒果图案

很显然,我们一定会有一个关于 Delineator(绘图器) 的类树,用来定义 X、Y和Z 的包装纸的基础样式,就像这样:

设计模式——1_3 装饰者(Decorator)_第3张图片

//绘图器接口
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的绘制方案");
    }
}

这个结构毋庸置疑,但是问题在于,要怎么把 草莓芒果 跟他们组合起来呢?


第一种解决方案

继承又被我们搬出来了,很容易就可以想到他的解决方式,就是这样:

设计模式——1_3 装饰者(Decorator)_第4张图片

虽然是错误示范,但是画的时候还是觉得好蠢

这个问题在前几篇文章中已经聊过很多次了,诸如 Delineator_X_Strawberry 这样的子类,并不是和其他子类有本质上的不同,这些子类之间的区别,无非就是对相同的数据进行排列组合而已

也就是说,如果我新增一个 葡萄味 的新口味,或者增加一个 W类型 的新饮料,那么这颗 绘图器类树 上的子类数量将会成倍增长

类爆炸 又出现了,这显然是一个非蠢即懒的解决方案


第二种解决方案

沿着之前的思路,既然继承在这里是在 排列组合,那我干脆就组合他如何,就像这样:

设计模式——1_3 装饰者(Decorator)_第5张图片

//绘图器抽象类
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(佐料绘图器) 类簇的方式,实现了口味和饮品之间的解耦。也实现了在程序运行的过程中动态组合口味和饮品的功能

这种看起来像 策略模式 的解决方案,确实像策略模式一样实现了通过切换算法簇,从而动态改变接口行为的目的

但是他的解耦度还不够,有两个大的问题

  1. SessoningDelineator 内的方法过于死板,只是在目前形势下可以完美契合需求

    比如说我现在新增 葡萄味 的饮品,而且规定 葡萄味 的包装纸是使用艺术字体进行打印。那你就只能在 SessoningDelineator 上新增一个类似 changeFont 这样的方法来切换字体

    但是这个方法对 草莓芒果 来说是毫无意义的,你的改变影响了他们

  2. 没有人规定口味只能有一种

    参考 农夫果园,有单种、两种甚至N种蔬果组合而成的风味

    那你会说,那我可以把单个 SessoningDelineator 对象,改成维护一个 SessoningDelineator 列表呀,然后在 draw 中循环调用他们不就可以了

    这样也不行,这样会导致你写入列表的顺序至关重要,他将决定风味的渲染顺序

所以这种方案只能解一时之需,我们还需要可以持续化发展的方法


装饰者解决方案

如果用装饰者解决这个问题,那么他会是这样的:

设计模式——1_3 装饰者(Decorator)_第6张图片

//绘图器接口
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 的绘制方法的方式,的的确确改变了他们最终的成品

而这正是一个标准的适配器模式实现



碎碎念

装饰者和继承

既然装饰者是继承关系的替代方案之一,那为什么在装饰者的实现中要使用继承呢?

答:继承有两个作用:

  1. 继承父类的 行为
  2. 继承父类的 类型

装饰者模式中使用继承是为了后者,为了让 client 在使用装饰者的时候可以直接使用装饰者类创建出来的对象,为了让装饰者可以融入主体的类簇中


装饰者和定制对象

通常我们定制一个对象,除了改变状态的方式之外,只能通过定义子类

装饰者让我们可以动态的去定制一个对象,而且不需要影响已有的类簇,这是很牛的一个方案

但是由于装饰者的拓展方向太多,当你需要定制的内容越来越多,你的装饰者子类就会越来越多,每一个小功能都意味着需要创建一个新的装饰者子类。这不算类爆炸,但也不是什么值得夸耀的事情

Java io流,这是特别经典的装饰者模式实现

你觉得Java的io流类库设计得如何呢?

事实上褒贬不一

他确实给我们提供了巨大的自由度,我们甚至可以自定义io装饰器,修改io流的行为。但这也导致我们每次用io流,基本都是操作复数对象,从执行动作后的close就可以看出来有多麻烦

还是那句话,要学会在合适的环境下使用合适的方法,再牛的手法都拯救不了不会变通的程序员


装饰者和大量对象

相信你也发现了,装饰者虽然避免了排列组合子类的情况的发生,但是由于每个装饰者类都不在一个体系中,所以每次使用装饰者,都意味着大量的对象在同一时间被创建,这也算是装饰者的缺陷之一


装饰者和汉堡包

你应该没见过麦当劳宣传自己用了什么牌子的生菜,因为重要的永远是夹在里面的肉

但这不代表包裹肉的面包和香菜就没有存在的价值,你能说鸡肉堡和鸡肉卷的味道是一样的吗?但是肉是一样的肉,他们的区别就是装饰在他们外层的内容所体现的

在世界这个大舞台上,绝大多数人都只是龙套而已。可是哪怕是电影里出境时间不到一秒的路人,都可以根据他的生平来写一部外传。所以哪怕是配角,是龙套,是装饰者,对他自己的故事而言,舞台中央的那个人,才是配角




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

你可能感兴趣的:(设计模式,设计模式,java,装饰器模式)