隔着“InputStream”看“装饰器模式”

从InputStream悟透装饰器模式

俄罗斯套娃,大家应该都知道。如果每套中的套娃之间的表情都是不一样的,那比如我现在想看笑脸的,我就只需要大的套小的一直套到笑脸的那一个。我觉得装饰器模式的经典实现模式就像俄罗斯套娃,每个套娃之间相互独立,但有可以组合使用。悟一悟,想一想,是不是有点儿像。JDK中不同的InputStream流就是装饰器模式经典的实现。

正文

1.日常使用InputStream

日常工作中,必不可少的就是对文件的读取,下面我就从读取文件的日常代码中分析InputStream。

# 案例1-不使用带有缓冲区的InputStream
public static void main(String[] args) {
    try (FileInputStream fileInputStream = new FileInputStream("/Users/weiyunpeng/Documents/test.txt")) {
        StringBuilder content = new StringBuilder();
        final byte[] bytes = new byte[10];
        int offset;
        while ((offset = fileInputStream.read(bytes)) != -1) {
            content.append(new String(bytes, 0, offset));
        }
        System.out.println("读取文件的内容是:" + content);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
# 案例2-使用带有缓冲区的InputStream
public static void main(String[] args) {
    try (FileInputStream fileInputStream = new FileInputStream("/Users/Documents/test.txt");
         BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {
        StringBuilder content = new StringBuilder();
        final byte[] bytes = new byte[10];
        int offset;
        while ((offset = bufferedInputStream.read(bytes)) != -1) {
            content.append(new String(bytes, 0, offset));
        }
        System.out.println("读取文件的内容是:" + content);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

上面的代码就是简单读取test.txt文件,将其文件内容输出到控制台。案例1和案例2几乎没有区别,输出的结果都一样,其中案例2使用到了FileInputStream和BufferedInputStream,并且FileInputStream是作为参数传递到BufferedInputStream的构造函数中的。这样就可以使用BufferedInputStream中特有的功能—字节缓冲区。画个图简单说明下前后的差别。

这是不使用BufferedInputStream流的层次图

image-20220313122541481

这是使用BufferedInputStream流后读取的层次图

image-20220313122732640

2.思考-为何JDK要通过组合的方式给不同的流添加不同的“功能特性”

假设,我们有这种给某个类添加一些特有功能的需求,我们会如何去设计呢?正常情况,我们可能会继承这个类,然后重写父类中的方法,加上特殊的功能。这样是可以实现需求,但是当需要很多种不同的特殊功能时,子类会越来越膨胀,会越来越多,难以维护,这种方式就会存在指数级别的复杂继承关系,而且各种特殊功能之间不能够自由灵活的组合(A特殊功能无法同时有B特殊功能,除非再次继承重写方法),很麻烦。

所以,这时候通过组合的方式就可以避免上面的问题,JDK中输入输出流相关的类就是这样实现的。原理其实很简单,存在一个父类(接口)InputStream,分别存在MyInputStream(按单个字节一个一个读取)和MyDecoratorInputStream(存在一定大小的字节数组作为缓冲区),它们具有各自不同读取流的特性。我们结合代码讲解下。下面这个抽象父类read,write,foo方法有个基础的实现。

//抽象父类
public abstract class InputStream {
    public void read(){
        System.out.println("inputStream 默认实现");
    }

    public void write(){
        System.out.println("inputStream 默认实现");
    }

    public void foo(){
        System.out.println("inputStream 默认实现");
    }
}
复制代码

下面存在两个子类,MyInputStream和MyDecoratorInputStream,MyDecoratorInputStream对原始的read,write方法做了功能增强。当需要使用赠强后的方法时,就只需要将MyInputStream的一个实例通过构造方法注入到MyDecoratorInputStream实例中,最后调用MyDecoratorInputStream实例的方法。就能实现在基础功能之上附加增强的方法,运行的结果贴在下面。

//子类---重写了父类中的所有方法
public class MyInputStream extends InputStream {
    @Override
    public void read(){
        System.out.println("MyInputStream 默认实现");
    }

    @Override
    public void write(){
        System.out.println("MyInputStream 默认实现");
    }

    @Override
    public void foo(){
        System.out.println("MyInputStream 默认实现");
    }
}
//装饰类--继承InputStream抽象类,foo方法没有重写
public class MyDecoratorInputStream extends InputStream{
    private InputStream inputStream;

    public MyDecoratorInputStream(InputStream inputStream){
        this.inputStream = inputStream;
    }

    @Override
    public void read(){
        //增强
        System.out.println("MyDecoratorInputStream read");
        //原始调用
        inputStream.read();
    }

    @Override
    public void write(){
        inputStream.write();
    }

    //foo方法我不重写
}
//调用
public class TestCode {
    public static void main(String[] args) {
        MyInputStream myInputStream = new MyInputStream();
        MyDecoratorInputStream myDecoratorInputStream = new MyDecoratorInputStream(myInputStream);
        myDecoratorInputStream.read();
        myDecoratorInputStream.write();
        myDecoratorInputStream.foo();
    }
}
复制代码

运行结果:

image-20220313132901763

最后,再思考一下,如果我的增强类只需要对父类中的一个方法进行增强,那对于父类中的其他方法,增强类还是需要实现一遍,假设MyInputStream只需要对InputStream中的read方法进行增强,但MyInputStream还是需要实现write方法,虽然方法体中只是调用super.write();所以针对上面的问题,JDK是如何解决的呢?查看下下面的代码,发现BufferedInputStream实现的是FilterInputStream类,而非InputStream。

public class BufferedInputStream extends FilterInputStream {
  .....
}
复制代码

大家可以看看FilterInputStream类,它提供了流的一些基本功能,所以如果有其他特殊的功能,增强类只需要继承FilterInputStream,并重写需要增强的方法。这样增强类就不需要关心哪些不要增强的方法了。

总结

虽然组合方式灵活的组装了各种不同的特殊功能,但是多层装饰的话也带来了复杂度。罗列下使用场景和装饰器模式的优缺点。

使用场景

  • 扩展一个类的功能。
  • 动态增加功能,动态撤销。

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。

求波关注,公众号:java精进天路

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享