【设计模式】装饰者模式?点过奶茶你就知道!

【设计模式】装饰器模式

继承滥用是一个问题,我们如何使用对象组合的方式,来实现运行时装饰类。熟悉装饰器模式,可以在不修改底层代码的情况下,对对象赋予新的内容。

1 场景

1.1 需求描述

奶茶店现在逐步都支持了线上点餐的方式,我们现在的目标就是实现一个简单的奶茶选择搭配生成对应搭配的类型选择结果与价格的一个功能。

我们都知道点一杯奶茶的使用的订单系统,基本如下:

(1)选择奶茶类型例如:乌龙奶茶(Wulong)、抹茶奶茶(Mocha)、可可奶茶(Keke)、招牌芋圆奶茶(yuyuan)。

点击图片之后进入选择对应奶茶的规格配置。

(2)选择份量:小杯、大杯

不同份量对应价格不同。

(3)选择配料内容例如:加燕麦(yanmai)、加椰果(yeguo)、加仙草(xiancao)、红豆(hongdou)。

其中配料一次可以选中多个,每选择一个都会对价格进行增加

(4)选择甜度:全糖、7分糖、3分糖、微糖

(5)选择温度:正常冰、去冰、温热

在选择完配置以后,总计会显示:

(1)已选规格:招牌芋圆奶茶、大杯、加椰果、3分糖、去冰

(2)奶茶价格:18

1.2 设计过程

直接想到的一种逐步的实现方式。

首先实现一个简单的选择奶茶品种功能的设计:

仅仅实现选择奶茶类型的功能,选择一个类型就会显示对应的设计。

image-20211010023120299

这里每一种的奶茶选中以后,都重新继承Beverage饮料抽象类,继承以后将这种奶茶的介绍设置在description。并且利用getDescription来进行返回这个对象的描述,而每个子类中对cost()方法进行重写,计算饮料的价格。

接下来又进行了一次的功能延伸,我们需要加上各种的配料内容,一种设计方式:
image-20211004141948601

每个cost都单独计算出奶茶与配料的价格,我们还没有进一步实现温度甜度分量的内容,这样明显不对。

这里类很多是不必要实现的,可以通过实例变量和继承的方式来写,因此有人提出,我们可以把这些调料放到基类Beverage饮料类中去。加上实例变量布尔值Boolean代表是否加入调料。那么设计方式可能是这样的:

image-20211004142027767

这样设计的方式有什么不好的地方?

(1)扩展性不好:如果出现新的配料,需要修改Beverage中的字段值,并且改变超类中的cost的计算方式(需要遍历所有的成员函数判断是否有这个配料)。

(2)会出现不必要的对象继承:如果开发出了一款果茶的饮料,我们继续去继承Beverage的类,那么会把燕麦的成员变量与方法hasYanmai继承下来。

1.3 问题分析

1、继承的思考:

(1)继承确实有很强大的功能,但是继承并不能实现最好的弹性与易于维护的方式。

(2)继承是在编译的时候静态决定的子类都会继承到对应的行为,利用组合的方式扩展对象可以在运行时动态进行扩展。

(3)通过组合动态组合对象,在写新代码加入的时候,不需要修改现有的代码,没有修改老代码扩展性好,并且也不容易引入新的bug。耦合度也会降低。这就是组合的优势。

2、类设计原则:开放-关闭原则

类应该对功能扩展开放,对类的修改关闭。

开放:欢迎使用需要的行为扩展类,如果需求有新的改变或者增加,直接在原有的类上进行扩展。

关闭:不能修改现有的代码,必须关闭对类字段的修改,以防止对过去的功能影响。

3、类设计方式的思考

对类新增字段修改原则?

对类的设计,要对类容易扩展,并且不修改现有的代码的情况下,就可以直接搭配出新的行为,最好不要对类中的变量进行随意的扩展,而是利用原有的类中的对象进行组合实现功能,万不得已才对类进行新增字段。

如何兼顾开放与关闭?

但是二者是比较难兼顾的,所以要利用比较合适的设计模式,例如我们之前使用了观察者模式,就可以在加入新的观察者的时候,不用修改扩展主题Subject中的代码。

如何将一个类设计为可以扩展并且禁止修改?

许多模式都是提供扩展的方法保护代码免于修改,装饰者模式就是完全遵循开放关闭原则。

每个部分都遵循开放-关闭原则?

通常很难做到面向对象设计同时具备开放性和关闭性,遵循开放-关闭原则会引入新的抽象层次,会增大代码的复杂度。但是我们在针对设计中最经常改变的地方,需要细心的设计并且应用开放-关闭原则,以达到长期的易于维护并且扩展的目标。每个地方都满足这个开放-关闭原则没必要,但是必要的地方应该应用。

2 装饰者模式

2.1 了解装饰者模式

1、引入装饰者模式

从1场景中我们发现了继承无法完全解决问题,会导致类数量爆炸、设计死板不易于扩展,类加入的新功能并不是所有类需要的。

这样我们引入一种新的模式:我们以Beverage作为主体,在运行时以配料来装饰奶茶。

装饰的步骤可以描述为:

(1)一杯芋圆(yuyuan)奶茶对象

(2)以椰果对象【装饰】奶茶对象

(3)以仙草对象【装饰】奶茶对象

(4)调用cost()方法,通过装饰对象之间的【依赖关系】,将调料的钱加上去。

所以现在的问题成为了怎么【装饰】和【依赖关系】怎么建立。我们可以将装饰者(例如这里的椰果、仙草)看做一个包装者,或者看成一层包装纸,将一杯奶茶包装起来。

2、装饰者装饰奶茶的过程图示

(1)一杯芋圆(yuyuan)奶茶对象

image-20211004145049264

(2)以椰果对象【装饰】奶茶对象

需要建立一个Yeguo装饰对象,并且将YuyuanNaiCha包裹起来对象。

image-20211004145629733

(3)以仙草对象【装饰】奶茶对象

需要建立一个Xiancao装饰对象,并且将Yeguo对象包装起来。

image-20211004150241103

(4)调用cost()方法,通过装饰对象之间的【依赖关系】,将调料的钱加上去。

过程:调用最外层的装饰者Xiancao的cost()就可以完成。

Xiancao的cost()先委托它所装饰的对象Yeguo的cost()方法计算,Yeguo的cost()委托它所装饰的对象YuyuanNaicha的cost()方法去计算,通过YuyuanNaicha计算出他的奶茶钱,返回Yeguo对象,加上椰果费用,再返回Xiancao加入仙草钱,最后返回合计金额。

image-20211004151740369

3、装饰者和装饰对象关系小结

(1)装饰者和装饰对象有相同的超类型。

超类型一样才可以相互反映才能包裹,例如均为Beverage对象,所以装饰者可以包裹装饰对象。

(2)可以用一个或者多个装饰者层层包装一个对象。

例如此处例子,我们使用Yeguo和Xiancao来包装YuyuanNaicha对象。

(3)装饰者和被装饰者有相同的超类型,所以在任何需要原始对象需要被包装的长河,都可以用装饰过的对象替代。

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

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

2.2 装饰者模式定义

1、定义

装饰者模式(Decorator Pattern):装饰者模式装饰者动态的将功能附加到被装饰者对象上。如果需要扩展功能,装饰者提供了比继承更加有弹性的替代方案。

2、使用装饰器模式的类图框架

(1)抽象类图

image-20211004163821439

(2)实例分析

我们使用装饰器模式的类图框架来设计我们的奶茶店选择规格的实现类图:

image-20211004165358202

3、继承?组合?

这里可能大家会产生疑惑,这个模式中仍然会使用到继承,大家可能认为我们这里是使用组合来取代继承,但是事实上在模式中仍然会使用到继承的方法。这是为什么呢?

我们类图中调味料装饰者(Yeguo/Yanmai/Hongdou/Xiancao)是扩展自Beverage类,使用到了继承。这样的目的是为了装饰者(调味料)和被装饰者(xxxNaicha)是同一的类型,有共同的超类。利用继承达到【类型匹配】的效果,而不是利用继承获得【行为】。行为是来自装饰者和基础的组件,或者与其他装饰者共同组合产生的行为。依赖继承只能在编译的时候静态决定,而组合是在运行时才知道具体的行为。

3 代码实践

使用装饰者模式实现场景。

3.1 简单奶茶店点单功能

1、饮料抽象类与配料抽象类

饮料抽象类:

public abstract class Beverage {
    String description = "Unknow Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();

}

配料抽象类(装饰者类):

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

2、饮料

饮料扩展自Beverage。

例如芋圆奶茶:

public class YuyuanNaicha extends Beverage{
    public YuyuanNaicha() {
        description = "YuyuanNaicha";
    }

    public double cost() {
        return 13;
    }
}

例如乌龙奶茶:

public class WulongNaicha extends Beverage{
    public WulongNaicha() {
        description = "WulongNaicha";
    }

    public double cost() {
        return 15;
    }
}

以此类推实现了抽象组件(Beverage),具体组件(xxxNaicha),也有了装饰者抽象组件(CondimentDecorator),下面实现具体的装饰者。

3、调料

实现具体的装饰者:

例如椰果:

/**
 * 椰果本身是一个装饰者 需要扩展CondimentDecorator
 */
public class Yeguo extends CondimentDecorator {
    /**
     * Yeguo引用一个Beverage
     * (1)用实例变量记录饮料被装饰者
     * (2)将被装饰者记录到实例变量中 这里使用的是将被装饰者作为构造器的参数 再使用构造器记录在实例变量中
     */
    Beverage beverage;
    public Yeguo (Beverage beverage) {
        this.beverage = beverage;
    }

    /**
     * 委托的做法将调料的内容记录到叙述当中
     */
    public String getDescription() {
        return beverage.getDescription() + ", Yeguo";
    }

    /**
     * 计算价钱并且将调味料加入到委托对象中
     * @return
     */
    public double cost() {
        return 2 + beverage.cost();
    }
}

例如仙草:

public class Xiancao extends CondimentDecorator{
    Beverage beverage;
    public Xiancao(Beverage beverage) {
        this.beverage = beverage;
    }
    public String getDescription() {
        return beverage.getDescription() + ", Xiancao";
    }

    public double cost() {
        return 3 + beverage.cost();
    }
}

例如红豆:

public class Hongdou extends CondimentDecorator{
    Beverage beverage;
    public Hongdou(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Hongdou";
    }

    public double cost() {
        return 4 + beverage.cost();
    }
}

4、运行订单组件测试代码

建立一个奶茶店的类,这样我们就可以通过点单的不同方式获取对应最后点单的结果。

public class NaichaStore {
    public static void main(String[] args) {
        Beverage beverage = new YuyuanNaicha();
        System.out.println(beverage.getDescription() + " price=" + beverage.cost() + "yuan");

        Beverage beverage2 = new WulongNaicha();
        beverage2 = new Yeguo(beverage2);
        beverage2 = new Yeguo(beverage2);
        beverage2 = new Xiancao(beverage2);
        System.out.println(beverage2.getDescription() + " price=" + beverage2.cost() + "yuan");

        Beverage beverage3 = new MochaNaicha();
        beverage3 = new Hongdou(beverage3);
        beverage3 = new Xiancao(beverage3);
        beverage3 = new Yeguo(beverage3);
        System.out.println(beverage3.getDescription() + " price=" + beverage3.cost() + "yuan");

    }
}

输出:

image-20211005093718340

之后介绍到工厂模式和生成器模式,会有更好的方式建立被装饰者对象。

这样我们就实现了一个简单的奶茶店点单功能。那么我们如何加上容量大小(大杯中杯小杯奶茶),因此这样要在原有的Beverage类中加上getSIze()和setSize()。如果店家希望调味料也按照不同奶茶杯容量进行收费,例如红豆的在大中小杯中分别收4 3 2 元,我们如何设计?再加上温度选择、甜度选择,如何设计呢?

3.2 需求描述完整实现

还需要实现选择饮料容量,并且还需要实现甜度和温度。

容量、温度和甜度都是单选的情况,因此可以不用设置在装饰类中。因此单独设置字段在Beverage中。

(1)Beverage饮料类

package Decorator.naichaStoreOrder;

/**
 *  饮料抽象类
 */
public abstract class Beverage {

    public enum Size {TALL, GRANDE, VENTI};

    public enum Sweetness {WHOLE, SEVENTY, THIRTY, TINY};

    public enum Temperature {NORMAL_ICE, REMOVE_ICE, WARM};

    Size size = Size.TALL;

    Sweetness sweetness = Sweetness.SEVENTY;

    Temperature temperature = Temperature.REMOVE_ICE;

    String description = "Unknow Beverage";

    public void setSize(Size size) {
        this.size = size;
    }

    public Size getSize() {
        return this.size;
    }

    public void setTemperature(Temperature temperature) {
        this.temperature = temperature;
    }

    public Temperature getTemperature() {
        return this.temperature;
    }

    public void setSweetness(Sweetness sweetness) {
        this.sweetness = sweetness;
    }

    public Sweetness getSweetness() {
        return this.sweetness;
    }

    public String getDescription() {
        return description;
    }

    public abstract double cost();

}

(2)装饰者类

package Decorator.naichaStoreOrder;

import Decorator.simpleStore.Beverage.*;

public abstract class CondimentDecorator extends Beverage {
    public Beverage beverage;

    public abstract String getDescription();

    public Size getSize() {
        return beverage.getSize();
    }
}

(3)装饰者

package Decorator.naichaStoreOrder;

public class Hongdou extends CondimentDecorator {
    public Hongdou(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Hongdou";
    }

    public double cost() {
        return 4 + beverage.cost();
    }
}

(4)奶茶

package Decorator.naichaStoreOrder;

public class KekeNaicha extends Beverage {
    public KekeNaicha() {
        description = "KekeNaicha";
    }

    public double cost() {
        return 16;
    }

}
package Decorator.naichaStoreOrder;

public class MochaNaicha extends Beverage {
    public MochaNaicha() {
        description = "MochaNaicha";
    }
    public double cost() {
        return 14;
    }
}

(5)商店

package Decorator.naichaStoreOrder;

import Decorator.naichaStoreOrder.Beverage.*;

public class NaichaStore {
    public static void main(String[] args) {
        Beverage beverage = new YuyuanNaicha();
        System.out.println(beverage.getDescription() + " price=" + beverage.cost() + "yuan");

        Beverage beverage2 = new WulongNaicha();
        beverage2 = new Yeguo(beverage2);
        beverage2 = new Yeguo(beverage2);
        beverage2 = new Xiancao(beverage2);
        System.out.println(beverage2.getDescription() + " price=" + beverage2.cost() + "yuan");

        Beverage beverage3 = new MochaNaicha();
        beverage3.setSize(Size.VENTI);
        beverage3.setSweetness(Sweetness.THIRTY);
        beverage3.setTemperature(Temperature.REMOVE_ICE);
        beverage3 = new Hongdou(beverage3);
        beverage3 = new Xiancao(beverage3);
        beverage3 = new Yeguo(beverage3);
        System.out.println(beverage3.getDescription()
                + ", size=" + beverage3.getSize()
                + ", sweetness=" + beverage3.getSweetness()
                + ", temperatur=" + beverage3.getTemperature()
                + ", price=" + beverage3.cost() + "yuan");

    }
}

image-20211005222339163

4 Java IO源码分析

4.1 输入流超类InputStream抽象类

IO流从超类是InputStream。实现了可关闭接口。

public abstract class InputStream implements Closeable {
    // 最大可跳过缓冲数组大小
    private static final int MAX_SKIP_BUFFER_SIZE = 2048;
    
    // 抽象读取方法 每次调用往后读取一个字节 返回int  (byte)int就可以转换为读入的字节
    // read(byte b[], int off, int len)会使用到这个来判断是否读取到字节
    public abstract int read() throws IOException;
    
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    
    public int read(byte b[], int off, int len) throws IOException {
        if(b == null) {
            // 如果存储读取到的字节的字节数组b[] 为空 那么抛出异常
            throw new NullPointerException();
        } else if(off < 0 || len < 0 || len > b.length - off) {
            // 如果off或者len不符合规范 过大或者过小则会抛出范围异常
            throw new IndexOutOfBoundsException();
        } else if(len == 0) {
            //如果读取的长度为0 那么返回0
            return 0;
        }
        // 调用读取方法read()进行读取 c代表读取到的字节
        int c = read();
        // 如果读取到的字节数为-1 表示没有可读的字节则返回-1 同时告知调用者 没有可读字节
        if(c == -1) {
            return -1;
        }
        // 将读取到的字节从b[]的off下标处进行存入
        b[off] = (byte)c;
        // 因为上面调用了一次read的时候已经读入了一个字节 因此
        int i = 1;
        try {
            // 循环读取直到i 不小于len
            for(; i < len; i++) {
                // 读取下一个字节
                c = read();
                // 如果一旦读不到字节了 则read返回-1跳出循环
                if(c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        // 返回读取到的字节数
        return i;
    }
    
    // 跳过n个字节 实质上是读取后进行抛弃
    public long skip(long n) throws IOException {
        // remaining为剩余要跳过的字节大小
        // 复制 跳过字节大小 到 剩余要跳过的字节大小
        long remaining = n;
        int nr;
		// 如果剩要跳过的字节数小于0则直接返回0 代表跳过了0个字节
        if(n <= 0) {
            return 0;
        }
        // 从要跳过的字节数 和最大允许跳过的字节中取最小值
        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        // 创建临时要跳过的字节大小的字节数组
        byte[] skipBuffer = new byte[size];
        // 如果剩余要跳过的字节大小>0则继续循环
        while(remaining > 0) {
            // 情况1:如果 要跳过的字节数remaining < 最大允许跳过的字节数size 则直接读取要跳过的字节数remaining大小的字节到临时数组
            // 情况2:如果 要跳过的字节数remaining > 最大允许跳过的字节数size 则读取最大允许跳过的字节数size到临时数组 并且循环直到读取完
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            // 如果read()返回值小于0 则表示没有可读的字节 直接跳出循环
            // 此情况只出现在当要跳过的字节大小 > 输入流剩余字节大小的时候
            if(nr < 0) {
                break;
            }
            // 如果是情况1 则相减后==0 相当于只执行一次
           	// 如果是情况2 则每次读取 最大允许跳过数 大小的字节,循环读取,直到读取/跳过到指定大小的字节数
            remaining -= nr;
        }
        // 总数-跳过后剩余未跳过的字节数=跳过的字节数  返回跳过了多少字节
        return n - remaining;
    }
    
    // 返回可读字节数(需要被子类重写)
    public int available() throws IOException {
        return 0;
    }
    
    // 关闭未实现
    public void close() throws IOException {}
    
    // 标记位置 以便于使用reset() 方法回滚该位置
    public synchronized void mark(int readlimit) {}
    
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    
    // 判断是否支持mark() reset()方法
    public boolean markSupported() {
        return false;
    }
    
}

4.2 FileterInputSTream被装饰者

被装饰者FilterInputSTream

public class FilterInputStream extends InputStream {
    // 组合了一个被装饰者
    protected volatile InputStream in;
    
    // 创建时传入一个被装饰者
    protected FilterInputStream(InputStream in) {this.in = in;}
    
    // 组合调用被装饰者的方法 完成对应的方法
    // 读取对单个字节(返回的是int 可以用(byte)int装换为byte)
    public int read() throws IOException {
        return in.read();
    }
    
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    
    public long skip(long n) throws IOException {
        return in.skip(n);
    }
    
    public int available() throws IOException {
        return in.available();
    }
    
    public void close() throws IOException {
        in.close();
    }
    
    public synchronized void mark(int readlimit) {
        in.mark(readlimit);
    }
    
    public synchronized void reset() throws IOException {
        in.reset();
    }
    
    public boolean markSupported() { return in.markSupported(); }
}

4.3 BufferedInputStream装饰者

BufferedInputStream是一个具体的装饰者,加入两种行为:利用缓冲输入来改变性能,利用readLine()方法来增强接口。

public class BufferedInputStream extends FilterInputStream {
    // 默认缓冲区大小
    private static int DEFAULT_BUFFER_SIZE = 8192;
    
    // JVM规定的缓存上限
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
    
    // 缓冲数组
    protected volatile byte buf[];
    
    // 原子引用字段修改器,在创建的时候指定了子类、字段类型、字段名称 可以使用这个类 对指定的类的字段buf进行原子替换操作
    private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdate = 
        								AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, "buf");
    
    // 索引大于buf[]数组中的最后一个有效字符的位置;非负数 小于buf[]数组的长度
    // buf[0] - buf[count - 1]是可以读取的字节
    // count最大允许读取的字节数
    protected int count;
    
    // 从buf[]中读取的下一个字节的索引,不能为负数,不能大于count,buf[pos]是要读取的下一个字节
 	protected int pos;   
    
    // 最后一次调用mark()方法时pos的值
    protected int markpos = -1;
    
    // mark后最大可读字节数,调用mark()方法 pos-markpos不能超过这个limit的值 否则reset()会失效,并且markpos重置为-1。
    protected int marklimit;
    
    // 如果in输入流不为空 则返回in (in属性在FilterInputStream中 被装饰者)
    private InputStream getInIfOpen() throws IOException {
        // in为FilterInputStream中的对象 用于扩展装饰者
        InputStream input = in;
        if(input == null) {
            throw new IOException("Stream closed");
        }
        return input;
    }
    
    // 如果缓冲数组不为空 则返回缓冲数组 否则抛出异常
    private byte[] getBufIfOpen() throws IOException {
        byte[] buffer = buf;
        if(buffer == null) {
            throw new IOEXception("Stream closed");
        }
        return buffer;
    }
    
    // 创建该类 需要传入被装饰者 一个InputStream的结点流,缓存大小为默认值
    public BufferedInputStream(InputStream in) {
		this(in, DEFAULT_BUFFER_SIZE);
    }
    
    public BUfferInputStream(InputStream in, int size) {
        super(in);
        if(size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    
    // 补充缓冲区
    public void fill() throws IOException {
        // 获取缓冲数组
        byte[] buffer = getBufIfOpen();
        
        // 如果markpos < 0 mark方法未生效 则设置下一个读取位置从0开始 没有mark()则重新读取缓冲数组
        if(markpos < 0) {
            pos = 0;
        } else if(pos >= buffer.length) {
            // 如果mark的时候已经读取了部分的数据 不是在还没开始读缓存数组的时候就调用mark()
            // 下面if 将缓存数组重置为 从标记位置开始读取的所有字节的数组 然后继续从之前的位置读取数组 放弃已经读取的并且已经mark之前的数据
            if(markpos > 0) {
                // mark()后 已经读取的字节
                int sz = pos - markpos;
                // 将buf[]数组的一些数据复制到自己的从0开始的位置
                System.arraycopy(buffer, markpos, buffer, 0, sz);
                // 将下一个读取索引设置为mark()后 已经读取的字节数
                // 也就上面的操作,相当于是:放弃了缓存数组中mark标记前的数据,然后还是从操作之前的位置读取
                pos = sz;
                markpos = 0;
                /**
                 * 因为之前的外层if,表明此时,已经是mark了,且把缓冲区读完了,
                 * 且,上面的if(markpos>0)没有执行,则表示之前mark时,还没有读取缓冲区数据,是在pos为0时mark的。
                 * 此处又判断出 缓冲区的长度 大于 mark的最大读取限制
                 * 也就是说,此时 上个mark后,读取的字节数已经超出 mark的最大读取限制,则将上个mark变为无效
                 */
            } else if(buffer.length >= marklimit) {
                // 缓存过大 将mark设置为无效
                markpos = -1;
                // 然后从头开始读取缓冲区
                pos = 0;
                /**
                 * 如果缓存数组长度 大于等于 最大缓存长度
                 * 也就是说,虽然 上个mark后,没有超出marklimit,但是数组的长度超出上限了
                 */
            } else if(buffer.length >= MAX_BUFFER_SIZE) {
                throw new OutOfMemoryError("Required array size too large");
                /**
                 * 如果上面的都没有成立
                 * 也就是说:该流被mark了;然后读取完了缓冲区所有的数据;然后mark也没有超出一些规定的大小;而且mark的位置不是缓冲区的0索引的位置,
                 * 这样,如果什么都不做,mark就失效了,所以就需要扩展缓冲区,让mark不失效
                 */
            } else {
                //扩大缓冲区
                int nsz = (pos <= MAX_BUFFER_SISZ - pos) ?  pos * 2 : MAX_BUFFER_SIZE;
                
                if(nsz > marklimit) nsz = marklimit;
                
                byte nbuf[] = new byte[nsz];
                
                //将原缓冲区中的所有数据拷贝到 该数组nbuf[]中去,拷贝的长度是pos(此时应该是原缓冲区的长度,反正就是已经读取了的数据)
                System.arraycopy(buffer, 0, nbuf, 0, pos);
                /**
                 * bufUpdater.compareAndSet(this, buffer, nbuf) 该方法第一个参数为要替换字段值的对象,第二个为期望值,第三个为新值;如果对象的字段的原值等于 期望值,则将其替换为 新值
                 * 此处是,如果该流的缓冲区到现在还是原先的值,就把缓冲区原子地替换为上面这个nbuf[],也就是扩展过的缓冲区
                 */
                // CAS
                if(!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                    throw new IOException("Stream closed");
                }
                
                //将当方前方法中的缓冲区替换为新的缓冲区
                buffer = nbuf;
            }
            //因为缓冲区大小变化了,所以count(最大允许读取字节数)也要变化,先让它等于pos,下面加上读取到的字节数后,也就是最大允许读取的字节数了
            count = pos;
            //使用被装饰者的方法将缓冲区读取满
            int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
            //如果读取到了字节,则将pos+读取到的字节数,也就是, 最大能读取的字节数(count)
            if(n > 0) count = n + pos;
        }   
    }
    
    // 读取下一个字节 不过对于这个缓冲流 这个方法会先判断缓冲区中是否还有数据 如果没有数据就一次性将缓冲区补满 然后读取的时候读取就是缓冲区的数据
    public synchronized int read() throws IOException {
        // 如果当前下个要读取的字节索引 超过或等于最大允许读取字节数 缓冲区满了
        if(pos >= count) {
            // 调用该方法解决缓冲区过小的问题 如果没有问题就是进行普通的重新读取缓冲区的操作
            fill();
            // 如果调用后还是超过或者等于 则表示没有可读字节了
            if(pos >= count) {
                return -1;
            }
        }
        // 如果缓冲区还有可读数据 则返回缓冲区中下一个字节的数据
        return getBufIfOpen()[pos++] & 0xff;
    }
    
    // 将数据读取到数组中 一般只读取一部分 如果有必要 也可以直接将缓冲区中的所有数据读取到该数组中
    private int read1(byte[] b, int off, int len) throws IOException {
        // 剩余可读字节数
        int avail = count - pos;
        // 如果缓冲区中没有可读的字节 则从流中补充
        if(avail <= 0) {
            // 如果读取的长度大于等于缓冲区大小,并且没有使用mark, 则不通过缓冲区 直接读取
            if(len >= getBufIfOpen().length && markpos < 0) {
                return getInIfOpen().read(b, off, len);
            }
            // 否则就补充缓冲区
            fill();
            // 然后再次判断可读字节数
            avail = count - pos;
            // 如果还是没有表示流中也读完了返回-1
            if(avail <= 0) return -1;
        }
        // 取剩余可读字节数和要读取的字节数的较小值
        int cnt = (avail < len) ? avail : len;
        // 拷贝/读取字节到b[]数组
        System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
        // 给下一个读取的字节索引 加上前面读取的字节数
        pos += cnt;
        // 返回读取的字节数
        return cnt;
    }
    
    public synchronized int read(byte[] b, int off, int len) throws IOException {
        getBufIfOpen();
        if((off | len | (off + len) | (b.length - (off + len))) < 0) {
            throw new IndexOutOfBoundsException();
        } else if(len == 0) {
            return 0;
        }
        int n = 0;
        for(;;) {
            // 使用read1()方法进行读取 返回读取到的字节数
            int nread = read1(b, off + n, len - n);
            // 如果没有可以读取的字节
            if(nread <= 0) {
                return (n == 0) ? nread : n;
            }
            n += nread;
            if(n >= len) {
                return n;
            }
            //调用被装饰者,如果流没有关闭,且可读字节数没了,也直接返回
            InputStream input = in;
            if (input != null && input.available() <= 0)
                return n;
        }
    }
    
    // 跳过n长度的字节
    public synchronized long skip(long n) throws IOException {
        getBufIfOpen();
        if(n <= n) {
            return n;
        }
        // 缓冲数组中剩余可读的字节数
        long avail = count - pos;
        // 如果没有可读字节了
        if(avail <= 0) {
            if(markpos < 0) {
                return getInIfOpen().skip(n);
            }
            // 填充缓冲数组
            fill();
            // 当缓冲区中 剩余可读字节数
            avail = count - pos;
            // 如果可用的字节数还是没有 则返回超过0个字节
            if(avail <= 0) {
                return 0;
            }
        }
        // 判断 当前剩余可读字节数 和要跳过的字节数的大小 跳过小的
        long skipped = (avail < n) ? avail : n;
        // 更改所以 也就是跳过对应字节数
        pos += skipped;
        // 返回跳过的字节数
        return skipped;
    }
    
    // 返回剩余可读取的字节数的估计值 不会被下一个调用此流的方法阻塞
    public synchronized int available() throws IOException {
        // 最大允许读取字节数 - 当前下一个要读取的字节索引 = 剩余可读取字节数
        // 缓冲数组中剩余可读取字节数
        int n = count - pos;
        // 被包装者 获取剩余可读字节数
        // 真正可读取字节数
        int avail = getInIfOpen().available();
        // 如果缓冲区剩余可读+流中剩余可读 > Integer.MAX_VALUE 则直接返回最大值否则返回两者之和。
        return n > (Integer.MAX_VALUE - avail) ? Integer.MAX_VALUE : n + avail;
    }
    
    public synchronized void mark(int readlimit) {
        marklimit = readlimit;
        markpos = pos;
    }
    
    public synchronized void reset() throws IOException {
        // 获取缓存数组,如果流已关闭,会引发异常
        getBufIfOpen();
        // 如果markpos<0,则表示mark()方法未使用,抛出异常
        if(markpos < 0) {
            throw new IOException("reaseting to invalid mark");
        }
        //执行到这步,表示进行重置操作,将 下个要读取字节索引 重置 为 标记索引
        pos = markpos;
    }
    
    // 返回流是否支持mark() reset()方法 因为支持所以直接返回true
    public boolean markSupported() {
        return true;
    }
 	// 关闭流 并且释放流相关的资源   
    public void close() throws IOException {
        byte[] buffer;
        while((buffer = buf) != null) {
            if(bufUpdater.compareAndSet(this, buffer, null)) {
                InputStream input = in;
                in = null;
                if(input != null) {
                    input.close();
                }
                return;
            }
        }
    }
}

4.4 自己定义一种Java IO装饰者

新建一种LowerCaseInputStream

public class LowerCaseInputStream extends FilterInputStream {
    public LowerCaseInputStream(InputStream in) {
        super(in);
    }

    // 读取一个byte
    public int read() throws IOException {
        int c = super.read();
        return (c == -1 ? c : Character.toLowerCase((char)c));
    }

    public int read(byte[] b, int offset, int len) throws IOException {
        int result = super.read(b, offset, len);
        for(int i = offset; i < offset + result; i++) {
            b[i] = (byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}

装饰者类进行包装我们读取文件的过程

public class InputTest {
    public static void main(String[] args) throws IOException {
        int c;
        try {
            // 设置InputStream 先用BufferedInputStream装饰 再用LowerCaseInputStream装饰
            InputStream in = new LowerCaseInputStream(
                        new BufferedInputStream(
                                new FileInputStream("F:\\IDEAfile\\DesignPatterns\\src\\Decorator\\JavaIO\\test.txt")));
            // 用流来读取 一直读到尾端
            while((c = in.read()) >= 0) {
                System.out.print((char) c);
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

test.txt路径为"F:\IDEAfile\DesignPatterns\src\Decorator\JavaIO\test.txt"

并且内容为:

image-20211010010507219

输出:

image-20211010013153253

5 小结

装饰者模式:动态的将功能加到被装饰对象上。想要扩展功能,装饰者提供了有别于继承的一种选择。

装饰者模式要点:

(1)继承属于扩展形式之一,但是不是达到弹性设计的最佳方式。设计中,应该允许为行为进行扩展,而不需要修改已有的代码。组合和委托可以用于在运行时动态的添加新的行为。

(2)装饰者模式也可以让我们扩展行为,装饰者模式有一群装饰者类,这些类用于包装具体组件。装饰者类反映出被装饰者的组件类型(实际上他们有相同的类型,都经过接口或者继承实现)。装饰者可以在被装饰者的行为前面/后面加上自己的行为,甚至将被装饰者的行为直接取代,达到自己的更新目的。

(3)装饰者模式会导致设计出现很多小对象,过度使用会导致程序过于复杂。

参考文献:

《Head First 设计模式》

完整代码已上传github:https://github.com/gaolijiemathcs/DesignPatterns

这是路遥知码力的第2篇公众号文章,希望能每周输出一篇有质量的公众号文章,欢迎关注交流!感谢阅读!

不积跬步无以至千里,下次见!

在这里插入图片描述

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您讲解一下如何使用Java中的装饰者模式来设计实现奶茶订单问题。 首先,我们可以定义一个基础的奶茶类,包含一些最基本的属性和方法,例如: ```java public abstract class MilkTea { String description = "Unknown Milk Tea"; public String getDescription() { return description; } public abstract double cost(); } ``` 接下来,我们可以定义一些具体的奶茶类,例如: ```java public class BlackMilkTea extends MilkTea { public BlackMilkTea() { description = "Black Milk Tea"; } public double cost() { return 10.0; } } public class GreenMilkTea extends MilkTea { public GreenMilkTea() { description = "Green Milk Tea"; } public double cost() { return 12.0; } } ``` 然后,我们可以定义一个装饰者类,用于对奶茶进行装饰,例如: ```java public abstract class MilkTeaDecorator extends MilkTea { public abstract String getDescription(); } ``` 接下来,我们可以定义一些具体的装饰类,例如: ```java public class PearlDecorator extends MilkTeaDecorator { private MilkTea milkTea; public PearlDecorator(MilkTea milkTea) { this.milkTea = milkTea; } public String getDescription() { return milkTea.getDescription() + ", add pearl"; } public double cost() { return milkTea.cost() + 2.0; } } public class PuddingDecorator extends MilkTeaDecorator { private MilkTea milkTea; public PuddingDecorator(MilkTea milkTea) { this.milkTea = milkTea; } public String getDescription() { return milkTea.getDescription() + ", add pudding"; } public double cost() { return milkTea.cost() + 3.0; } } ``` 最后,我们可以在客户端代码中进行测试,例如: ```java public class Client { public static void main(String[] args) { MilkTea milkTea = new BlackMilkTea(); milkTea = new PearlDecorator(milkTea); milkTea = new PuddingDecorator(milkTea); System.out.println(milkTea.getDescription() + " " + milkTea.cost()); } } ``` 以上就是使用Java中的装饰者模式来设计实现奶茶订单问题的示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值