Head First 设计模式(3)----装饰者模式

本文参照《Head First 设计模式》,转载请注明出处
对于整个系列,我们按照这本书的设计逻辑,使用情景分析的方式来描述,并且穿插使用一些问题,总结的方式来讲述。并且所有的开发源码,都会托管到github上。
项目地址:https://github.com/jixiang52002/HFDesignPattern

回顾上一篇文章讲解了设计模式中常用的一种模式------观察者模式。并结合气象站设计进行实战解析,并且从自己设计到JAVA自带设计模式做了讲解。想要了解的朋友可以回去回看一下。
Head First 设计模式(2)---观察者(Observer)模式

本章我们会继续前面的话题,有关典型的继承滥用问题。这一章会讲解如何使用对象组合的方式,如何在运行时候做装饰类。在熟悉装饰技巧后,我们能够在原本不修改任何底层的代码,却可以给原有对象赋予新的职能。你会说,这不就是“装饰者模式”。没错,接下来就是装饰者模式的ShowTime时间。

1.前言

欢迎来到星巴兹咖啡,该公司是世界上以扩张速度最快而闻名的咖啡连锁店。但是最近这家著名的咖啡公司遇到一个巨大的问题,因为扩展速度太快了,他们准备更新订单系统,以合乎他们的饮料供应需求。

他们本来的设计方式如下:


Beverage类结构

然后客户购买咖啡时,可以要求在其中加入任何调料,例如:奶茶,牛奶,豆浆。星巴兹根据业务需求会计算相应的费用。这就要求订单系统必须考虑到这些调料的部分。

然后我们就看到他们的第一个尝试设计:

各种饮料的类关系图

是不是有一种犯了密集恐惧症的感觉,整完全就是“类爆炸”。
那么我们分析一下,这种设计方式违反了什么设计原则?没错,违反了以下两个原则:

第二设计原则
针对于接口编程,不针对实现编程

第三设计原则
多用组合,少用继承

那么我们应该怎么修改这个设计呢?

2.利用继承对Beverage类进行改造

首先,我们考虑对基类Beverage类进行修改,我们根据前面“类爆炸”进行分析。主要饮料包含各种调料(牛奶,豆浆,摩卡,奶泡。。。。)。
所以修改后的Beverage类的结构如下:

Beverage带调料后的实现

Beverage类具体实现如下:

public class Beverage {
    protected String description;//饮料简介
    
    protected boolean milk=false;//是否有牛奶
    
    protected boolean soy=false;//是否有豆浆
    
    protected boolean cocha=false;//是否有摩卡
    
    protected boolean whip=false;//是否有奶泡
    
    protected double milkCost=1.01;//牛奶价格
    
    protected double soyCost=1.03;//豆浆价格
    
    protected double cochaCost=2.23;//摩卡价格
    
    protected double whipCost=0.89;//奶泡价格
    
    
    public String getDescription() {
        return description;
    }


    public void setDescription(String description) {
        this.description = description;
    }


    public boolean hasMilk() {
        return milk;
    }


    public void setMilk(boolean milk) {
        this.milk = milk;
    }


    public boolean hasSoy() {
        return soy;
    }


    public void setSoy(boolean soy) {
        this.soy = soy;
    }


    public boolean hasCocha() {
        return cocha;
    }


    public void setCocha(boolean cocha) {
        this.cocha = cocha;
    }


    public boolean hasWhip() {
        return whip;
    }


    public void setWhip(boolean whip) {
        this.whip = whip;
    }
    
    


    public double getCochaCost() {
        return cochaCost;
    }


    public void setCochaCost(double cochaCost) {
        this.cochaCost = cochaCost;
    }


    public double getWhipCost() {
        return whipCost;
    }


    public void setWhipCost(double whipCost) {
        this.whipCost = whipCost;
    }


   

    public double cost(){
        
        double condiments=0.0;
        if(hasMilk()){//是否需要牛奶
            condiments+=milkCost;
        }
        if(hasSoy()){//是否需要豆浆
            condiments+=soyCost;
        }
        if(hasCocha()){//是否需要摩卡
            condiments+=cochaCost;
        }
        if(hasWhip()){//是否需要奶泡
            condiments+=whipCost;
        }
        return condiments;
    }

}

实现其中一个子类DarkRoast:

public class DarkRoast extends Beverage{

    public DarkRoast(){
        description="Most Excellent Dark Roast!";
    }
    
    public double cost(){
        return 1.99+super.cost();
    }
}

看起来很完美,也能满足现有的业务需求,但是仔细思考一下,真的这样设计不会出错?

回答肯定是会出错。

  • 第一,一旦调料的价格发生变化,会导致我们队原有代码进行大改。
  • 第二,一旦出现新的调料,我们就需要加上新的方法,并需要改变超类Beverage类中cost()方法。
  • 第三,如果星巴兹咖啡研发新的饮料。对于这些饮料而言,某些调料可能并不合适,但是子类仍然会继承那些本就不合适的方法,例如我就想要一杯水,加奶泡(hasWhip)就不合适。
  • 第四,如果用户需要双倍的摩卡咖啡,又应该怎么办呢?

3.开放-关闭原则

到这里,我们可以推出最重要的设计原则之一:

第五设计原则:
类应该对拓展开放,对修改关闭。

那么什么是开放,什么又是关闭开放就是允许你使用任何行为来拓展类,如果需求更改(这是无法避免的),就可以进行拓展!关闭在于我们花费很多时间完成开发,并且已经测试发布,针对后续更改,我们必须关闭原有代码防止被修改,避免造成已经测试发布的源码产生新的bug。

综合上述说法,我们的目标在于允许类拓展,并且在不修改原有代码的情况下,就可以搭配新的行为。如果能实现这样的目标,带来的好处将相当可观。在于代码会具备弹性来应对需求改变,可以接受增加新的功能用来实现改变的需求。没错,这就是拓展开放,修改关闭。

那么有没有可以参照的实例可以分析呢?有,就在第二篇我们介绍观察者模式时,我们介绍到可以通过增加新的观察者用来拓展主题,并且无需向原主题进行修改。

我们是否需要每个模块都设计成开放--关闭原则?不用,也很难办到(这样的家伙可以称为“不用设计模式会死病”)。因为想要完全符合开放-关闭原则,会引入大量的抽象层,增加原有代码的复杂度。我们应该区分设计中可能改变的部分和不改变的部分(第一设计原则),针对改变部分使用开放--关闭原则。

4.装饰模式

这里,就到了开放--关闭原则的运用模式-----装饰者模式。首先我们还是从星巴兹咖啡的案例来做一个简单的分析。
分析之前两个版本(类爆炸 和 继承大法)的实现方式,并不能适用于所有的子类。

这就需要一个新的设计思路。这里,我们将以饮料为主,然后运行的时候以饮料来“装饰”饮料。举个栗子,如果用户需要摩卡和奶泡深焙咖啡,那么要做的是:

  • 拿一个深焙咖啡(DarkRosat)对象

  • 以摩卡(Mocha)对象装饰它

  • 以奶泡(Whip)对象装饰它

  • 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去。

具体的实现我们用一张图来展示

  • 首先我们构建DarkRoast对象


    DarkRoast对象
  • 假如顾客需要摩卡(Mocha),再建立一个Mocha对象,并用DarkRoast对象包起来。


    Mocha对象
  • 如果顾客也想要奶泡(Whip),就建立一个Whip装饰者,并将它用Mocha对象包起来。


    Mocha对象
  • 最后运算客户的账单的时候,通过最外层的装饰者Whip的cost()就可以办得到。Whip的cost()会委托他的装饰对象(Mocha)计算出价格,再加上奶泡(Whip)的价格。

计算用户的账单

通过对星巴兹咖啡的设计方案分析,我们可以发现,所有的装饰类都具备以下几个特点:

  • 装饰者和被装饰对象有相同的超类型。

  • 你可以用一个或多个装饰者包装一个对象。

  • 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。

  • 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。

  • 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象

什么是装饰模式呢?我们首先来看看装饰模式的定义:

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

定义虽然已经定义了装饰者模式的“角色”,但是未说明怎么在我们的实现中如何使用它们。我们继续在星巴兹咖啡中来熟悉相关的操作。

装饰者模式类图

其中装饰者层级可以无限发展下去,不是如图中一般两层关系。并且组件也并非只有一个,可以存在多个。

现在我们就在星巴兹咖啡里运用装饰者模式:


使用装饰模式的星巴兹咖啡

到这里,我们队装饰者模式已经有了一个基本的认识。那么我们已经解决了上面提到的四个问题:

  • 第一,一旦调料的价格发生变化,会导致我们队原有代码进行大改。
  • 第二,一旦出现新的调料,我们就需要加上新的方法,并需要改变超类Beverage类中cost()方法。
  • 第三,如果星巴兹咖啡研发新的饮料。对于这些饮料而言,某些调料可能并不合适,但是子类仍然会继承那些本就不合适的方法,例如我就想要一杯水,加奶泡(hasWhip)就不合适。
  • 第四,如果用户需要双倍的摩卡咖啡,又应该怎么办呢?

那么根据第四个问题,假如我们需要双倍摩卡豆浆奶泡拿铁咖啡时,该如何去运算账单呢?首先,我们先把前面的深度烘焙摩卡咖啡的设计图放在这里。


深度烘焙摩卡咖啡

然后我们只需要将Mocha的装饰者加一,即可


双倍摩卡豆浆奶泡拿铁咖啡

实现星巴兹咖啡代码

前面已经把设计思想都设计出来了,接下来是将其具体实现了。首先从Beverage类下手

public abstract class Beverage1 {
    String description="Unknown Beverage";
    
    public String getDescription(){
        return description;
    }
    
    public abstract double cost();
}

Beverage类非常简单,然后再实现Condiment(调料类),该类为抽象类,也为装饰者类

public abstract class CondimentDecorator extends Beverage1{

    //所有的调料装饰者都必须重新实现 getDescription()方法。 
    public abstract String getDescription();
}

前面已经有了饮料的基类,那么我们来实现一些具体的饮料类。首先从浓缩咖啡(Espresso))开始,这里需要重写cost()方法和getDescription()方法

public class Espresso extends Beverage1{
    
    public Espresso(){
        //为了要设置饮料的描述,我 们写了一个构造器。记住, description实例变量继承自Beverage1
        description="Espresso";
    }
    
    public double cost() {
        //最后,需要计算Espresso的价钱,现在不需要管调料的价钱,直接把Espresso的价格$1.99返回即可。
        return 1.99;
    }
}

再实现一个类似的饮料HouseBlend类。

public class HouseBlend extends Beverage1{

    public HouseBlend(){
        description="HouseBlend";
    }
    
    public double cost() {
      
        return 0.89;
    }
}

重新设计DarkRoast1

public class DarkRoast1 extends Beverage1{
   
   public DarkRoast1(){
       description="DarkRoast1";
   }
   
   public double cost() {
     
       return 0.99;
   }

}

接下来就是调料的代码,我们一开始已经实现了抽象组件类(Beverage),有了具体的组件(HouseBlend),也有了已经完成抽象装饰者(CondimentDecorator)。现在只需要实现具体的装饰者。首先我们先完成摩卡(Mocha)

public class Mocha extends CondimentDecorator{
    /**
     * 要让Mocha能够引用一个Beverage,采用以下做法
     * 1.用一个实例记录饮料,也就是被装饰者
     * 2.想办法让被装饰者(饮料)被记录在实例变量中。这里的做法是:
     * 把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     */
    Beverage1 beverage;
    
    public Mocha(Beverage1 beverage) {
        this.beverage=beverage;
    }
    
    public String getDescription() {
        //这里将调料也体现在相关参数中
        return beverage.getDescription()+",Mocha";
    }
    
    /**
     * 想要计算带摩卡的饮料的价格,需要调用委托给被装饰者,以计算价格,
     * 然后加上Mocha的价格,得到最终的结果。
     */
    public double cost() {
        return 0.21+beverage.cost();
    }
    
    

}

还有奶泡(Whip)类

public class Whip extends CondimentDecorator{
    /**
     * 要让Whip能够引用一个Beverage,采用以下做法
     * 1.用一个实例记录饮料,也就是被装饰者
     * 2.想办法让被装饰者(饮料)被记录在实例变量中。这里的做法是:
     * 把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     */
    Beverage1 beverage;
    
    public Whip(Beverage1 beverage) {
        this.beverage=beverage;
    }
    
    public String getDescription() {
        //这里将调料也体现在相关参数中
        return beverage.getDescription()+",Whip";
    }
    
    /**
     * 想要计算带奶泡的饮料的价格,需要调用委托给被装饰者,以计算价格,
     * 然后加上Whip的价格,得到最终的结果。
     */
    public double cost() {
        return 0.22+beverage.cost();
    }
}

豆浆Soy类

public class Soy extends CondimentDecorator{
    /**
     * 要让Soy能够引用一个Beverage,采用以下做法
     * 1.用一个实例记录饮料,也就是被装饰者
     * 2.想办法让被装饰者(饮料)被记录在实例变量中。这里的做法是:
     * 把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     */
    Beverage1 beverage;
    
    public Soy(Beverage1 beverage) {
        this.beverage=beverage;
    }
    
    public String getDescription() {
        //这里将调料也体现在相关参数中
        return beverage.getDescription()+",Soy";
    }
    
    /**
     * 想要计算带豆浆的饮料的价格,需要调用委托给被装饰者,以计算价格,
     * 然后加上Soy的价格,得到最终的结果。
     */
    public double cost() {
        return 0.21+beverage.cost();
    }
}

接下来就是调用测试类,具体实现如下:

public class StarbuzzCoffe {
    
    public static void main(String[] args) {
        //订购一杯Espresso,不需要调料,打印他的价格和描述
        Beverage1 beverage=new Espresso();
        System.out.println(beverage.getDescription()+"$"
                +beverage.cost());
        
        //开始装饰双倍摩卡+奶泡咖啡
        Beverage1 beverage2=new DarkRoast1();
        beverage2=new Mocha(beverage2);
        beverage2=new Mocha(beverage2);
        beverage2=new Whip(beverage2);
        
        System.out.println(beverage2.getDescription()+"$"
                +beverage2.cost());
        
        //
        Beverage1 beverage3=new HouseBlend();
        beverage3=new Soy(beverage3);
        beverage3=new Mocha(beverage3);
        beverage3=new Whip(beverage3);
        
        System.out.println(beverage3.getDescription()+"$"
                +beverage3.cost());
        
        
    }
}

运行结果:


运行结果

到这里,我们已经完成装饰者模式对于星巴兹咖啡的改造。

5.Java中的真实装饰者

前面已经研究了装饰者模式的原理和实现方式,那么在JAVA语言本身是否有装饰者模式的使用范例呢,答案是肯定有的,那就是I/O流。

第一次查阅I/O源码,都会觉得类真多,而且一环嵌一环,阅读起来会非常麻烦。但是只要清楚I/O是根据装饰者模式设计,就很容易理解。我们先来看一下一个范例:

读取文件

分析一下,其中BufferedInputStream及LineNumberInputStream都扩展自
FilterInputStream,而FilterInputStream是一个抽象的装饰类。这样看有些抽象,我们将其中的类按照装饰者模式进行结构化,方便理解。

java.io类

我们发现,和星巴兹的设计相比,java.io其实并没有多大的差异。但是从java.io流我们也会发现装饰者模式一个非常严重的"缺点":使用装饰者模式,常常会造成设计中有大量的小类,数量还非常多,这对于学习API的程序员来说就增加了学习难度和学习成本。但是,懂得装饰者模式以后会非常容易理解和设计相关的类。

6.设计自己的IO类

在理解装饰者模式和java.io的设计后,我们将磨炼下自己的熟悉程度,没错,就是自己设计一个Java I/O装饰者,需求如下:

编写一个装饰者,把输入流内的所有大写字符转成小写。举例:当读
取“ ASDFGHJKLQWERTYUIOPZXCVBNM”,装饰者会将它转成“ asdghjklqwertyuiopzxcvbnm”。具体的办法在于扩展FilterInputStream类,并覆盖read()方法就行了。

public class LowerCaseInputStream extends FilterInputStream{
    
    public LowerCaseInputStream(InputStream inputStream){
        super(inputStream);
    }
    
    
    public int read() throws IOException {
       int c=super.read();
       //判断相关的字符是否为大写,并转为小写
       return (c==-1?c:Character.toLowerCase((char)c));
    }
    
    /**
     * 
     *针对字符数组进行大写转小写操作
     * @see java.io.FilterInputStream#read(byte[], int, int)
     */
    public int read(byte[] b, int off, int len) throws IOException {
        int result=super.read(b,off,len);
        for(int i=off;i<off+result;i++){
            b[i]=(byte) Character.toLowerCase((char)b[i]);
        }
        return result;
    }

}

接下来我们构建测试类InputTest

public class InputTest {
    public static void main(String[] args) {
        int c;
        try {
            InputStream inputStream=new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
            while((c=inputStream.read())>=0){
                System.out.print((char)c);
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

其中test.txt的内容可以自行编辑,放在项目根目录下我的内容原文为:


test.txt原文

运行结果为:

运行结果

7.总结

至此,我们已经掌握了装饰者模式的相关知识点。总结一下:

第五设计原则
类应该对拓展开放,对修改关闭。

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

相应的资料和代码托管地址https://github.com/jixiang52002/HFDesignPattern

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容