设计模式系列----装饰模式的理解

装饰模式

提到装饰,我们先来想一下生活中有哪些装饰: 女生的首饰:戒指、耳环、项链等装饰品 家居装饰品:粘钩、镜子、壁画、盆栽等
我们为什么需要这些装饰品呢?很容易想到是为了美,戒指、耳环、项链、壁画、盆栽等都是为了提高颜值或增加美观度。但粘钩、镜子不一样,它们是为了方便我们挂东西、洗漱。

所以我们可以总结出装饰品共有两种功能:

增强原有的特性:我们本身就是有一定颜值的,添加装饰品提高了我们的颜值。同样,房屋本身就有一定的美观度,家居装饰提高了房屋的美观度。
添加新的特性:在墙上挂上粘钩,让墙壁有了挂东西的功能。在洗漱台装上镜子,让洗漱台有了照镜子的功能。

并且,我们发现装饰品并不会改变物品本身,只是起到一个锦上添花的作用。装饰模式也一样,它的主要作用就是:
1、增强一个类原有的功能
2、为一个类添加新的功能

并且 装饰模式也不会改变原有的类。
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。

1. 用于增强功能的装饰模式

我们用程序来模拟一下戴上装饰品提高我们颜值的过程:
新建颜值接口:

public interface IBeauty {
	int getBeautyValue();
}

新建 Me 类,实现颜值接口:

public class Me implements IBeauty {
	@Override
	public int getBeautyValue() {
		return 100;
}
}

戒指装饰类,将 Me 包装起来:

public class RingDecorator implements IBeauty {
	private final IBeauty me;
	public RingDecorator(IBeauty me) {
		this.me = me;
	}
	@Override
	public int getBeautyValue() {
		return me.getBeautyValue() + 20;
	}
}

客户端测试:

public class Client {
	@Test
	public void show() {
		IBeauty me = new Me();
		System.out.println("我原本的颜值:" + me.getBeautyValue());
		IBeauty meWithRing = new RingDecorator(me);
		System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue());
	}
}

运行程序,输出如下:
我原本的颜值:100
戴上了戒指后,我的颜值:120
这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:
耳环装饰类:

public class EarringDecorator implements IBeauty {
	private final IBeauty me;
	public EarringDecorator(IBeauty me) {
		this.me = me;
	}
	@Override
	public int getBeautyValue() {
		return me.getBeautyValue() + 50;
	}
}

项链装饰类:

public class NecklaceDecorator implements IBeauty {
	private final IBeauty me;
	public NecklaceDecorator(IBeauty me) {
	this.me = me;
}
@Override
	public int getBeautyValue() {
		return me.getBeautyValue() + 80;
	}
}

客户端测试:
运行程序,输出如下:
我原本的颜值:100
戴上了项链后,我的颜值:180
戴上耳环、戒指、项链后,我的颜值:250
戴上戒指、项链后,我的颜值:200

可以看到,装饰器也实现了 IBeauty 接口,并且没有添加新的方法,也就是说这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能,这种装饰模式称之为 透明装饰模式,由于没有改变接口,也没有新增方法,所以透明装饰模式可以无限装饰

装饰模式是 继承 的一种替代方案。本例如果不使用装饰模式,而是改用继承实现的话,戴着戒指的Me 需要派生一个子类、戴着项链的 Me 需要派生一个子类、戴着耳环的 Me 需要派生一个子类、戴着戒指 + 项链的需要派生一个子类…各种各样的排列组合会造成类爆炸。而采用了装饰模式就只

public class Client {
@Test
	public void show() {
		IBeauty me = new Me();
		System.out.println("我原本的颜值:" + me.getBeautyValue());
		// 随意挑选装饰
		IBeauty meWithNecklace = new NecklaceDecorator(me);
		System.out.println("戴上了项链后,我的颜值:" + meWithNecklace.getBeautyValue());
		// 多次装饰
		IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new Earri
		System.out.println("戴上耳环、戒指、项链后,我的颜值:" + meWithManyDecorators.getBe
		// 任意搭配装饰
		IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new 		RingDecorator(me));
		System.out.println("戴上戒指、项链后,我的颜值:" + meWithNecklaceAndRing.getBeauty
}
}

需要为每个装饰品生成一个装饰类即可,所以说就 增加对象功能 来说,装饰模式比生成子类实现更
为灵活。

2. 用于添加功能的装饰模式

我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:
新建房屋接口:

public interface IHouse {
	void live();
}

房屋类:

public class House implements IHouse{
@Override
	public void live() {
		System.out.println("房屋原有的功能:居住功能");
	}
}

新建粘钩装饰器接口,继承自房屋接口:

public interface IStickyHookHouse extends IHouse{
	void hangThings();
}

粘钩装饰类:

public class StickyHookDecorator implements IStickyHookHouse {
	private final IHouse house;
	public StickyHookDecorator(IHouse house) {
		this.house = house;
	}
	@Override
	public void live() {
		house.live();
	}
	@Override
	public void hangThings() {
		System.out.println("有了粘钩后,新增了挂东西功能");
	}
}

客户端测试:

public class Client {
	@Test
	public void show() {
		IHouse house = new House();
		house.live();
		IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
		stickyHookHouse.live();
		stickyHookHouse.hangThings();
	}
}

运行程序,显示如下:
房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘钩后,新增了挂东西功能
这就是用于 新增功能 的装饰模式。我们在接口中新增了方法:hangThings,然后在装饰器中将House 类包装起来,之前 House 中的方法仍然调用 house 去执行,也就是说我们并没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式。

为什么叫半透明呢?由于新的接口 IStickyHookHouse 拥有之前 IHouse 不具有的方法,所以我们如果要使用装饰器中添加的功能,就不得不区别对待 装饰前的对象和装饰后的对象。也就是说客户端要使用新方法,必须知道具体的装饰类 StickyHookDecorator,所以这个装饰类对客户端来说是
可见的、不透明的。而被装饰者不一定要是 House,它可以是实现了 IHouse 接口的任意对象,所以被装饰者对客户端是不可见的、透明的。由于一半透明,一半不透明,所以称之为半透明装饰模
式。
我们可以添加更多的装饰器:
新建镜子装饰器的接口,继承自房屋接口:

public interface IMirrorHouse extends IHouse {
	void lookMirror();
}

镜子装饰类:

public class MirrorDecorator implements IMirrorHouse{
	private final IHouse house;
	public MirrorDecorator(IHouse house) {
		this.house = house;
	}
	@Override
	public void live() {
		house.live();
	}
	@Override
	public void lookMirror() {
		System.out.println("有了镜子后,新增了照镜子功能");
	}
}

客户端测试:

public class Client {
	@Test
	public void show() {
		IHouse house = new House();
		house.live();
		IMirrorHouse mirrorHouse = new MirrorDecorator(house);
		mirrorHouse.live();
		mirrorHouse.lookMirror();
	}
}

运行程序,输出如下:
房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了镜子后,新增了照镜子功能
现在我们仿照 透明装饰模式 的写法,同时添加粘钩和镜子装饰试一试:

public class Client {
	@Test
	public void show() {
		IHouse house = new House();
		house.live();
		IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
		IMirrorHouse houseWithStickyHookMirror = new 		MirrorDecorator(stickyHookHouse);
		houseWithStickyHookMirror.live();
		houseWithStickyHookMirror.hangThings(); // 这里会报错,找不到 hangThings 方法
		houseWithStickyHookMirror.lookMirror();
	}
}

我们会发现,第二次装饰时,无法获得上一次装饰添加的方法。原因很明显,当我们用
IMirrorHouse 装饰器后,接口变为了 IMirrorHouse,这个接口中并没有 hangThings 方法。
那么我们能否让 IMirrorHouse 继承自 IStickyHookHouse,以实现新增两个功能呢?
可以,但那样做的话两个装饰类之间有了依赖关系,那就不是装饰模式了。装饰类不应该存在依赖关系,而应该在原本的类上进行装饰。这就意味着,半透明装饰模式中,我们无法多次装饰。

有的同学会问了,既增强了功能,又添加了新功能的装饰模式叫什么呢?
—— 举一反三,肯定是叫全不透明装饰模式!
—— 并不是!只要添加了新功能的装饰模式都称之为 半透明装饰模式,他们都具有不可以多次装饰
的特点。仔细理解上文半透明名称的由来就知道了,“透明”指的是我们无需知道被装饰者具体的
类,既增强了功能,又添加了新功能的装饰模式仍然具有半透明特性。
看了这两个简单的例子,是不是发现装饰模式很简单呢?恭喜你学会了 1 + 1 = 2,现在你已经掌握
了算数的基本思想,接下来我们来做一道微积分题练习一下。

I/O 中的装饰模式

I/O 指的是 Input/Output,即输入、输出。我们以 Input 为例。先在 src 文件夹下新建一个文件
readme.txt,随便写点文字:

禁止套娃
禁止禁止套娃
禁止禁止禁止套娃

然后用 Java 的 InputStream 读取,代码一般长这样:

public void io() throws IOException {
	InputStream in = new BufferedInputStream(new 			FileInputStream("src/readme.txt"));
	byte[] buffer = new byte[1024];
	while (in.read(buffer) != -1) {
	System.out.println(new String(buffer));
	}
	in.close();
}

这样写有一个问题,如果读取过程中出现了 IO 异常,InputStream 就不能正确关闭,所以我们要用 try…finally 来保证 InputStream 正确关闭:

public void io() throws IOException {
	InputStream in = null;
	try {
		in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
		byte[] buffer = new byte[1024];
		while (in.read(buffer) != -1) {
		System.out.println(new String(buffer));
		}
	} finally {
		if (in != null) {
		in.close();
		}
	}
}

观察获取 InputStream 这句代码:

InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));

是不是和我们之前多次装饰的代码非常相似:
事实上,查看 I/O 的源码可知,Java I/O 的设计框架便是使用的 装饰者模式,InputStream 的继承
关系如下:
在这里插入图片描述

其中,InputStream 是一个抽象类,对应上文例子中的 IHouse,其中最重要的方法是 read 方法,
这是一个抽象方法:

public void io() throws IOException {
	try (InputStream in = new BufferedInputStream(new 	FileInputStream("src/readme.txt"))
	byte[] buffer = new byte[1024];
	while (in.read(buffer) != -1) {
	System.out.println(new String(buffer));
	}
}
}
// 多次装饰
IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorators())
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
// ...
}

这个方法会读取输入流的下一个字节,并返回字节表示的 int 值(0~255),返回 -1 表示已读到末尾。由于它是抽象方法,所以具体的逻辑交由子类实现。

上图中,左边的三个类 FileInputStream、ByteArrayInputStream、ServletInputStream 是
InputStream 的三个子类,对应上文例子中实现了 IHouse 接口的 House。
右下角的三个类 BufferedInputStream、DataInputStream、CheckedInputStream 是三个具体的
装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。
FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下
InputStream:

public class FilterInputStream extends InputStream {
	protected volatile InputStream in;
	protected FilterInputStream(InputStream in) {
	this.in = in;
}
public int read() throws IOException {
	return in.read();
}
//...
}

我们以 BufferedInputStream 为例。原有的 InputStream 读取文件时,是一个字节一个字节读取
的,这种方式的执行效率并不高,所以我们可以设立一个缓冲区,先将内容读取到缓冲区中,缓冲
区读满后,将内容从缓冲区中取出来,这样就变成了一段一段读取,用内存换取效率。
BufferedInputStream 就是用来做这个的。它继承自 FilterInputStream:

public class BufferedInputStream extends FilterInputStream {
	private static final int DEFAULT_BUFFER_SIZE = 8192;
	protected volatile byte buf[];
	public BufferedInputStream(InputStream in) {
		this(in, DEFAULT_BUFFER_SIZE);
	}
public BufferedInputStream(InputStream in, int size) {
	super(in);
	if (size <= 0) {
	throw new IllegalArgumentException("Buffer size <= 0");
	}
	buf = new byte[size];
}
//...
}

我们先来看它的构造方法,在构造方法中,新建了一个 byte[] 作为缓冲区,从源码中我们看到,
Java 默认设置的缓冲区大小为 8192 byte,也就是 8 KB。
然后我们来查看 read 方法:

public class BufferedInputStream extends FilterInputStream {
//...
	public synchronized int read() throws IOException {
		if (pos >= count) {
		fill();
		if (pos >= count)
		return -1;
		}
		return getBufIfOpen()[pos++] & 0xff;
	}
private void fill() throws IOException {
// 往缓冲区内填充读取内容的过程
//...
}
}

在 read 方法中,调用了 fill 方法,fill 方法的作用就是往缓冲区中填充读取的内容。这样就实现了
增强原有的功能。
在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以
BufferedInputStream 使用的是 透明的装饰模式。
DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readInt、readLong 等方法,所以 DataInputStream 使用的是 半透明装饰模式。
理解了 InputStream 后,再看一下 OutputStream 的继承关系,相信大家一眼就能看出各个类的作用了:
这就是装饰模式,注意不要和适配器模式混淆了。两者在使用时都是包装一个类,但两者的区别其
实也很明显:
纯粹的适配器模式 仅用于改变接口,不改变其功能,部分情况下我们需要改变一点功能以适配新接口。但使用适配器模式时,接口一定会有一个 回炉重造 的过程。
装饰模式 不改变原有的接口,仅用于增强原有功能或添加新功能,强调的是 锦上添花。
掌握了装饰者模式之后,理解 Java I/O 的框架设计就非常容易了。但对于不理解装饰模式的人来说,各种各样相似的 InputStream 非常容易让开发者感到困惑。这一点正是装饰模式的缺点:
容易造成程序中有大量相似的类。虽然这更像是开发者的缺点,我们应该做的是提高自己的技术,掌握了这个设计模式之后它就是我们的一把利器。现在我们再看到 I/O 不同的 InputStream 装饰类,只需要关注它增强了什么功能或添加了什么功能即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值