zoukankan      html  css  js  c++  java
  • 三、装饰者模式 (Decorator Pattern)《HeadFirst设计模式》读书笔记

      装饰者模式可以实现在不修改任何代码的情况下,给对象赋予新的功能。

      1.使用继承的缺陷:

      假设一家咖啡店的类设计是有一个抽象父类Beverage(饮料):

    public abstract class Beverage {
        //由子类设置,用来描述具体饮料
        protected String description;
        //计算总价的抽象方法
        public abstract double cost();
        public String getDescription() {
            return description;
        }
    }

      具体的cost()方法由子类实现,比如DarkRost(深烘培咖啡)的价格是0.1:

    public class DarkRoast extends Beverage {
        public DarkRoast(String description) {
            super.description = description;
        }
        @Override
        public double cost() {
            return 0.1;
        }
    }

      现在进行功能扩展,要求可以加入各种配料,如milk、soy(豆浆)、mocha(摩卡)、whip(奶泡),如果我们把每一种配料组合都写成一个子类(如DarkRostWithMilk)的话,会造成类数量的猛增,而且在增加配料或者修改价格的时候会造成维护上的困难。

      因此进一步的,我们可以在父类Beverage中预先定义好每种配料,对应一个布尔值表示是否添加,父类的cost()也本身提供,直接将用到的配料价格加起来返回。

    public abstract class NewBeverage {
        //由子类设置,用来描述具体饮料
        protected String description;
        //配料变量
        protected int milk;
        protected int soy;
        protected int mocha;
        protected int whip;
        //构造方法
    
        public NewBeverage(String description, int milk, int soy, int mocha, int whip) {
            this.description = description;
            this.milk = milk;
            this.soy = soy;
            this.mocha = mocha;
            this.whip = whip;
        }
    
        //计算配料的总价格
        public double cost(){
            return milk*CondimentCost.MILK_COST
                    + soy*CondimentCost.SOY_COST
                    + mocha*CondimentCost.MOCHA_COST
                    + whip*CondimentCost.WHIP_COST;
        }
        //省略getter和setter方法
    }

       子类重写cost()方法,在其中加入自己的价格。

    public class NewDarkRoast extends NewBeverage {
        public NewDarkRoast(String description, int milk, int soy, int mocha, int whip) {
            super(description, milk, soy, mocha, whip);
        }
        @Override
        public double cost() {
            return super.cost() + CondimentCost.NEW_DARK_ROAST;
        }
    }

      这里将价格统一放到了一个常量类中:

    public class CondimentCost {
        //不同咖啡种类价格
        public static double NEW_DARK_ROAST = 0.1;
        //配料价格
        public static double MILK_COST = 0.01;
        public static double SOY_COST = 0.01;
        public static double MOCHA_COST = 0.01;
        public static double WHIP_COST = 0.01;
    }

      不过这样计算得到的结果不太对,小数点后面有很多位数,后来换成了BigDecimal类型通过String传参得到的结果是准确的,这里就不展开了。

      上面这种方法虽然不用写很多具体的子类,修改价格也可以直接在常量类中改,但是如果要新添加配料的话还是要到父类中去修改代码。除此之外,父类中包含了所有的配料和他们的getter/setter方法,子类可能只需要其中的几个但是却全部继承下来了。装饰者模式该出场了。

      2.引出装饰者模式

      如上图所示,先用装饰者类Mocha和Whip都继承它想要装饰的父类Beverage,首先Mocha包裹住DarkRoast对象,重写cost()方法,在方法内部调用DarkRoast的cost()方法,并将自己的价钱加进去;然后在外层同样的,Whip也包裹住Mocha对象,并在自己的cost()方法中调用Mocha的cost()方法并将自己的价钱加进去。通过这样一层层的包裹实现了cost()方法的增强。

      这里你可能有两个疑问:

        1.”包裹“具体指什么,怎么实现

        2.为什么装饰者类Mocha和Whip都要继承想要装饰的类的抽象父类Beverage呢

       首先第一个问题,”包裹“实际上指的就是组合,在Mocha内部声明类型为Beverage的成员变量,可以通过构造方法将具体的子类传进来,再调用它的cost()方法。通过具体的代码实现来看一下:

    public class Mocha extends Beverage {
        private Beverage beverage;
        public Mocha(Beverage beverage) {
            this.beverage = beverage;
        }
        @Override
        public double cost() {
            return beverage.cost() + CondimentCost.MOCHA_COST;
        }
    }
    public class Whip extends Beverage {
        private Beverage beverage;
        public Whip(Beverage beverage) {
            this.beverage = beverage;
        }
        @Override
        public double cost() {
            return beverage.cost() + CondimentCost1.WHIP_COST;
        }
    }

      测试一下:

    public class test03 {
        public static void main(String[] args) {
            Beverage darkRoast = new DarkRoast("i am darkroast");
            Beverage mocha = new Mocha(darkRoast);
            Beverage whip = new Whip(mocha);
            System.out.println("darkroast + mocha + whip total cost: " + whip.cost());
        }
    }

       

      这就是装饰者模式的使用了,通过这个例子也可以看出,之所以让装饰者类继承目标类的抽象父类,是因为想让装饰者也可以被包裹,即装饰者同时也是个被装饰者。

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

      而书中和上面代码示例有些不同的是抽象了一个装饰者的父类CondimentDecorator,由这个父类继承Beverage,再让Mocha、Whip等继承这个父类。类的关系如下:

      那么为什么中间加一层呢?我的理解是这样的:

        首先这样层级关系更清晰,明确抽象者类有哪些,不会和原本Beverage真正的子类混在一起;

        还有虽然我们在例子中是想要增强cost()方法的功能,但是我们还是要把其它需要用到的原本Beverage中的方法重写一下,并通过装饰的方式调用具体的Beverage的子类去执行。举个例子:

    public abstract class Beverage {
        //由子类设置,用来描述具体饮料
        protected String description;
        //计算总价的抽象方法
        public abstract double cost();
        public String getDescription() {
            return description;
        }
    }

      上面的getDescription()方法,如果我们在装饰者类中不重写的话,那么实际调用这个方法的时候,就不会委托给Beverage的具体子类去做,而是调用Beverage中的这个默认的方法,得到的description自然就是null,因此不管是不是需要增强的方法,都要去重写,如下所示:

    public class Mocha extends Beverage {
        private Beverage beverage;
        public Mocha(Beverage beverage) {
            this.beverage = beverage;
        }
        @Override
        public double cost() {
            return beverage.cost() + CondimentCost1.MOCHA_COST;
        }
    
        @Override
        public String getDescription() {
            return beverage.getDescription();
        }
    }

      这样如果我们想要装饰DarkRoast对象,才会在真正执行的时候调用到DarkRoast的getDescription()方法,同样的在Whip中我们也要这样实现,那么如果这样的方法有很多,装饰者类也有很多的话,写起来就会很麻烦,所以可以抽象出一个父类,在父类中将一些方法预先都准备好,就不用再在具体的装饰者类中去重写了。

      在JDK的输入输出流中就使用了装饰者模式,如下是用BuffedInputStream读取文件的一个demo(这里使用的JDK8,所以用的try-with-resource异常处理方式,在try代码块结束后自动释放资源):

    public class ReadFilesDemo {
        public static void main(String[] args) throws IOException {
            try (InputStream fis = new FileInputStream("D:/learnspace/demo.txt");
            InputStream bis = new BufferedInputStream(fis)) { int len; byte[] bytes = new byte[1024]; while ((len = bis.read(bytes)) != -1) { //将读取到的内容直接输出 System.out.println(new String(bytes, 0, len)); } } catch (IOException e) { e.printStackTrace(); } } }

      这里InputStream就是我们想要扩展功能的类,想通过buffer实现更高效的读取,BufferedInputStream就是一个具体的装饰者类,它首先继承了FilterInputStream,而FilterInputStream继承了InputStream,在FilterInputStream中包含了很多重写的方法,只是简单的在方法内部通过多态的方式委托给实际InputStream的具体子类去做了,因为FilterInputStream也有很多子类,这样通过继承可以减少代码的重复。这里举个例子:

    public class FilterInputStream extends InputStream {
        protected volatile InputStream in;
        //构造方法 
        protected FilterInputStream(InputStream in) {
            this.in = in;
        }
        //重写了read(),只是简单调用了in的read()方法
        public int read() throws IOException {
            return in.read();
        }
        ...
    }

      而具体的增强read()方法的代码是在FilterInputStream的子类BufferedInputSream中重写的:

    public synchronized int read(byte b[], int off, int len)
            throws IOException
        {
            getBufIfOpen(); // Check for closed stream
            if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return 0;
            }
    
            int n = 0;
            for (;;) {
                int nread = read1(b, off + n, len - n);
                if (nread <= 0)
                    return (n == 0) ? nread : n;
                n += nread;
                if (n >= len)
                    return n;
                // if not closed but no bytes available, return
                InputStream input = in;
                if (input != null && input.available() <= 0)
                    return n;
            }
        }

      3.总结:

        装饰者模式在继承之外提供了一种新的方式拓展被装饰者的行为,但同时也会有一些问题,比如会产生很多小类,会在实例化组件时增加复杂度(要创建很多装饰者类),还有类型问题(书中P104页写的,没太看明白具体指什么),因此要合理的使用装饰者模式。

     

  • 相关阅读:
    JQuery之在线引用
    SpringBoot之durid连接池配置
    VueJs之事件处理器
    VueJs之样式绑定
    VueJs之判断与循环监听
    PTA 7-8 暴力小学(二年级篇)-求出4个数字 (10分)
    PTA 7-7 交替字符倒三角形 (10分)
    PTA 7-5 阶乘和 (10分)
    PTA 7-4 哥德巴赫猜想 (10分)
    PTA 7-3 可逆素数 (15分)
  • 原文地址:https://www.cnblogs.com/advancedcz/p/13235321.html
Copyright © 2011-2022 走看看