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

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

回顾上一篇文章讲解了设计模式中经常使用的一种模式------观察者模式。并结合气象站设计进行实战解析,而且从本身设计到JAVA自带设计模式作了讲解。想要了解的朋友能够回去回看一下。git

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

1.前言

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

他们原本的设计方式以下: 编程

Beverage类结构

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

而后咱们就看到他们的第一个尝试设计:数组

各类饮料的类关系图

是否是有一种犯了密集恐惧症的感受,整彻底就是“类爆炸”。 那么咱们分析一下,这种设计方式违反了什么设计原则?没错,违反了如下两个原则:bash

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

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

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

#利用继承对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)就不合适。
  • 第四,若是用户须要双倍的摩卡咖啡,又应该怎么办呢?

2.开放-关闭原则

到这里,咱们能够推出最重要的设计原则之一:

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

那么什么是开放,什么又是关闭?开放就是容许你使用任何行为来拓展类,若是需求更改(这是没法避免的),就能够进行拓展!关闭在于咱们花费不少时间完成开发,而且已经测试发布,针对后续更改,咱们必须关闭原有代码防止被修改,避免形成已经测试发布的源码产生新的bug。

综合上述说法,咱们的目标在于容许类拓展,而且在不修改原有代码的状况下,就能够搭配新的行为。若是能实现这样的目标,带来的好处将至关可观。在于代码会具有弹性来应对需求改变,能够接受增长新的功能用来实现改变的需求。没错,这就是拓展开放,修改关闭。

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

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

3.装饰模式

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

这就须要一个新的设计思路。这里,咱们将以饮料为主,而后运行的时候以饮料来“装饰”饮料。举个栗子,若是影虎须要摩卡和奶泡深焙咖啡,那么要作的是:

  • 拿一个深焙咖啡(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的装饰者加一,便可

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

4.实现星巴兹咖啡代码

前面已经把设计思想都设计出来了,接下来是将其具体实现了。首先从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());
		
		
	}
}
复制代码

运行结果:

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

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

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

读取文件

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

java.io类

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

5.设计本身的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原文

运行结果为:

运行结果

6.总结

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

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

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

相应的资料和代码托管地址github.com/jixiang5200…

相关文章
相关标签/搜索