最近在总结学习Java I/O相关知识点,I/O应用的场景比较多,不仅存在各种I/O源端和想要与之通信的接收端(文件、控制台、网络链接等),而且还需要支持多种不同方式的通信(顺序、随机存取、缓冲、二进制、按字符、按行、按字等)。
Java类库的设计者通过创建大量的类来解决这个难题,这里面用到了装饰器这一设计模式。关于设计模式,之前也有学习过,但是因为比较抽象,加上实际工作中应用较少,所以学习效果往往并不是很好,相信大多数人都有这种感觉。我觉得学习设计模式还是需要结合实际应用才会有更深的理解,而工作中用到各种各样的设计模式的场景毕竟不是很多,所以结合一些源码中对设计模式应用的例子来学习我觉得是一种折衷但不失为效果较好的方式。本文会先总结一下装饰器这一设计模式,然后结合其在Java I/O类库设计中的应用来进行学习,相信可以加深对这一设计模式的理解。
好吃的肉夹馍
首先请原谅一个吃货用这种方式来讲设计模式。上学的时候经常去学校门口的小摊上买里脊肉夹馍,这种食物对于我这种来自南方的同学来说很新奇,所以常常会去买。那时候上学比较节俭,一般都只要里脊(便宜),偶尔会加个鸡蛋、烤肠(需要加钱),对这个有些印象,因为摊主每次都是根据顾客定制的需求来算价格的。
好了,本文不是准备讲美食的,这只是一个亲身经历,留存在脑海中罢了。因为现在的工作是和编程相关的,所以对于很多生活中的事情我都习惯通过设计将其进行抽象(希望通过学以致用来锻炼自己的设计能力,因为这种能力不是一天两天就能构学好的,需要长期的磨练积累,说远了。。)。同样,对于里脊肉夹馍的价格问题也是可以抽象成类图来表示:
如上图,定义一个抽象类ChineseHamburger代表肉夹馍,小摊卖的所有夹馍都需继承自此类,有两个方法:
- getDescription(),抽象方法,可以返回是什么肉夹馍,由子类实现;
- cost()方法是抽象的,由子类来实现;
FilletChineseHamburger继承自ChineseHamburger,代表里脊肉夹馍,实现cost()方法来返回肉夹馍的价格。
好了,这只是最简单的模型,我们常常会有比如加个鸡蛋、加根烤肠等等需求,对应肉夹馍的价格也是不一样的,这样怎么办呢,我们可以直接增加几个子类代表对应的夹馍,这时候类图就像下面这样了:
看起来很容易就满足了需求,但是如果哪天摊主开发一种新的菜品比如鸡柳、生菜,或者我们既想加鸡蛋又想加里脊呢?按照这种方式我们是不是需要提供很多子类来实现各自的计价,想象一下与日俱增的子类数量,你是不是要崩溃了?
看来这种单纯通过继承的解决方案确实存在问题:类爆炸,光写这些类都是一项很大的开发工作量,而且还要考虑到以后的维护,问题肯定会越来越多,那怎么办?
我们需要作出一些改变:以夹馍为主体,然后在运行时用材料来“装饰”肉夹馍。比如说,如果顾客想要里脊鸡蛋肉夹馍,那么,可以这样,先来一个夹馍,以里脊对象装饰它,再以鸡蛋对象装饰它,调用cost()方法,里面会依赖委托将所材料的加钱加上去。这样,每次有不一样的需求,只需要将对应的材料进行装饰即可,类似如下的步骤:
我们可以用类图抽象一下来表示:
如上图,我们定义一个普通夹馍(SimpleChineseHamburger)。再定义一个装饰器Decorator,其包含一个夹馍对象,并可以对其进行“装饰”(就是委托其进行计价),这样一来我们每多加一种材料,只需要多装饰一次即可,避免了重复设计大量的相似类。这,就是装饰器模式的应用。
什么是装饰器模式
装饰器模式的说明:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。原文是:
Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality.
我们来看一下类图:
在类图中,各个角色的说明如下:
Component,抽象构件
Component是一个接口或者抽象类,是定义我们最核心的对象,也可以说是最原始的对象,比如上面的肉夹馍。
ConcreteComponent,具体构件,或者基础构件
ConcreteComponent是最核心、最原始、最基本的接口或抽象类Component的实现,可以单独用,也可将其进行装饰,比如上面的简单肉夹馍。
Decorator,装饰角色
一般是一个抽象类,继承自或实现Component,在它的属性里面有一个变量指向Component抽象构件,我觉得这是装饰器最关键的地方。
ConcreteDecorator,具体装饰角色
ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,它们可以把基础构件装饰成新的东西,比如把一个普通肉夹馍装饰成鸡蛋里脊肉夹馍。
光解释比较抽象,我们再来看看代码实现,先看抽象构件:
public abstract class Component{ // 抽象地方法 public abstract void cost(); }
然后是具体基础构件:
public class ConcreteComponent extends Component{ @Override public void cost(){ // do something ... } }
抽象装饰角色:
public abstract class Decorator extends Component{ private Component component = null; public Decorator(Component component){ this.component = component; } @Override public void cost(){ this.component.cost(); } }
具体装饰角色:
public class ConcreteDecorator extends Decorator{ public ConcreteDecorator(Component component){ super(component); } // 定义自己的修饰逻辑 private void decorateMethod(){ // do somethind ... } // 重写父类的方法 public void cost(){ this.decorateMethod(); super.cost(); } }
我们可以通过一个具体例子来看一下装饰器模式是如何运行的:
public class DecoratorDemo{ public static void main(String[] args){ Component component = new ConcreteComponent(); // 第一次修饰,比如,加鸡蛋,加1块 component = new ConcreteDecorator(component); // 第二次修饰,比如,加烤肠,加2块 component = new ConcreteDecorator(component); // 修饰后运行,将钱加在一起 component.cost(); } }
装饰器模式在Java I/O系统中的实现
前面总结了这么多,再从大神们的作品中找一个实际应用例子吧,毕竟那是经历实战检验的,肯定是有道理的。嗯,在平时的留意中我发现Java I/O系统的设计中用到了这一设计模式,因为Java I/O类库需要多种不同功能的组合。这里我就以InputStream为例简单说明一下,同样我们还是来看一下其类图:
InputStream作为抽象构件,其下面大约有如下几种具体基础构件,从不同的数据源产生输入:
- ByteArrayInputStream,从字节数组产生输入;
- FileInputStream,从文件产生输入;
- StringBufferInputStream,从String对象产生输入;
- PipedInputStream,从管道产生输入;
- SequenceInputStream,可将其他流收集合并到一个流内;
FilterInputStream作为装饰器在JDK中是一个普通类,其下面有多个具体装饰器比如BufferedInputStream、DataInputStream等。我们以BufferedInputStream为例,使用它就是避免每次读取时都进行实际的写操作,起着缓冲作用。我们可以在这里稍微深入一下,站在源码的角度来管中窥豹。
FilterInputStream内部封装了基础构件:
protected volatile InputStream in;
而BufferedInputStream在调用其read()读取数据时会委托基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲,在源码中可以很清楚的看到这一切:
public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; } private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ if (markpos > 0) { /* can throw away early part of the buffer */ int sz = pos - markpos; System.arraycopy(buffer, markpos, buffer, 0, sz); pos = sz; markpos = 0; } else if (buffer.length >= marklimit) { markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ } else if (buffer.length >= MAX_BUFFER_SIZE) { throw new OutOfMemoryError("Required array size too large"); } else { /* grow buffer */ int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? pos * 2 : MAX_BUFFER_SIZE; if (nsz > marklimit) nsz = marklimit; byte nbuf[] = new byte[nsz]; System.arraycopy(buffer, 0, nbuf, 0, pos); if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { throw new IOException("Stream closed"); } buffer = nbuf; } count = pos; // 看这行就行了,委托基础构件来进行更底层的操作 int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; } private InputStream getInIfOpen() throws IOException { InputStream input = in; if (input == null) throw new IOException("Stream closed"); return input; }
这部分的代码很多,这里我们没有必要考虑这段代码的具体逻辑,只需要看到在BufferedInputStream的read方法中通过getInIfOpen()获取基础构件从而委托其进行更底层的操作(在这里是读取单个字节)就可以说明本文所要说的一切了。
至于I/O类库中的其他设计诸如OutputStream、Writer、Reader,是一致的,这里就不再赘述了。
总结
本文介绍了装饰器模式,其有如下优点:
- 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无需知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
- 装饰器模式是继承关系的一个替代方案。我们看装饰类Decorator,不管装饰多少层,返回的对象还是Component(因为Decorator本身就是继承自Component的),实现的还是is-a的关系。
- 装饰模式可以动态地扩展一个实现类的功能,比如在I/O系统中,我们直接给BufferedInputStream的构造器直接传一个InputStream就可以轻松构件一个带缓冲的输入流,如果需要扩展,我们继续“装饰”即可。
但是也有其自身的缺点:
多层的装饰是比较复杂的。为什么会复杂?你想想看,就像剥洋葱一样,你剥到最后才发现是最里层的装饰出现了问题,可以想象一下工作量。这点从我使用Java I/O的类库就深有感受,我只需要单一结果的流,结果却往往需要创建多个对象,一层套一层,对于初学者来说容易让人迷惑。
本文结合Java I/O类库的设计来总结了装饰器模式的相关知识点,希望通过这种方式能够加深你对设计模式的理解,希望能帮到你^_^。