从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流的层次图
这是使用BufferedInputStream流后读取的层次图
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();
}
}
复制代码
运行结果:
最后,再思考一下,如果我的增强类只需要对父类中的一个方法进行增强,那对于父类中的其他方法,增强类还是需要实现一遍,假设MyInputStream只需要对InputStream中的read方法进行增强,但MyInputStream还是需要实现write方法,虽然方法体中只是调用super.write();所以针对上面的问题,JDK是如何解决的呢?查看下下面的代码,发现BufferedInputStream实现的是FilterInputStream类,而非InputStream。
public class BufferedInputStream extends FilterInputStream {
.....
}
复制代码
大家可以看看FilterInputStream类,它提供了流的一些基本功能,所以如果有其他特殊的功能,增强类只需要继承FilterInputStream,并重写需要增强的方法。这样增强类就不需要关心哪些不要增强的方法了。
总结
虽然组合方式灵活的组装了各种不同的特殊功能,但是多层装饰的话也带来了复杂度。罗列下使用场景和装饰器模式的优缺点。
使用场景:
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。