定义
装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则。
即:在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
简言之装饰者模式的简单实现方式:装饰者中存放一个被装饰对象的引用(属性)。
- 接口A,
- 被装饰对象:一个实现类B,即被装饰对象。
- 装饰者抽象类:装饰者抽象类C实现A,并在其中存放一个A的引用。
- 具体装饰类:具体的装饰者实现类D继承C,完成对被装饰对象的装饰。
举例
需求:
现在有一个星巴克订单项目:
比如我可以点Espresso+豆浆+2份牛奶,然后计算它的价格,我也可以只点一份Espresso,然后计算它的价格。
解决方案1:
做一个抽象类,里面有一个cost抽象方法,然后将所有的组合穷举成类。
问题:
- 这样设计会造成类爆炸
- 如果添加一种调料,又要添加很多类
解决方案2:
将是否添加调料设计成布尔型,放到Drink类中,在单点咖啡中通过控制true和fasle来判断是否添加,这样做大大减少了类的数量
问题:如果新增调料时,代码的维护量还是很大。
解决方案3:装饰者模式
代码:
Drink抽象类:
public abstract class Drink {
public String des; // 藐视
private int price; // 价格 单位:分
// 计算费用的抽象方法
public abstract int cost();
// 省略getset方法
}
单点咖啡的父类:
public class Coffee extends Drink {
@Override
public int cost() {
return super.getPrice();
}
}
各个单点咖啡类:即被装饰类
public class Espresso extends Coffee{
public Espresso() {
setDes("意大利咖啡");
setPrice(100);
}
}
public class LongBlack extends Coffee{
public LongBlack() {
setDes("LongBlack");
setPrice(200);
}
}
public class ShortBlack extends Coffee{
public ShortBlack() {
setDes("ShortBlack");
setPrice(300);
}
}
添加类的父类:即装饰类:
public class Decorator extends Drink {
private Drink drink;
public Decorator(Drink drink) { // 组合
this.drink = drink;
}
@Override
public int cost() {
// getPrice 自己价格,drink.cost,单点咖啡的价格
return super.getPrice() + drink.cost();
}
public String getDes() {
// drink.getDes():被装饰者信息
return super.getDes() + " " + super.getPrice() + " " + drink.getDes();
}
}
添加类:
public class Chocolate extends Decorator{
public Chocolate(Drink drink) {
super(drink);
setDes("巧克力");
setPrice(10);
}
}
public class Milk extends Decorator{
public Milk(Drink drink) {
super(drink);
setDes("牛奶");
setPrice(20);
}
}
public class Soy extends Decorator{
public Soy(Drink drink) {
super(drink);
setDes("豆浆");
setPrice(30);
}
}
订单生成类:
public class StarBucks {
public static void main(String[] args) {
// 装饰者模式下的订单: 2份巧克力+1一份牛奶的LongBlack
// 点一份LongBlack
Drink order = new LongBlack();
System.out.println("费用1=" + order.cost());
System.out.println("描述1=" + order.getDes());
// order加入一份牛奶
order = new Milk(order);
System.out.println("费用2=" + order.cost());
System.out.println("描述2=" + order.getDes());
// 再加入一份牛奶
order = new Milk(order);
System.out.println("费用3=" + order.cost());
System.out.println("描述3=" + order.getDes());
// 再加入一份巧克力
order = new Chocolate(order);
System.out.println("费用4=" + order.cost());
System.out.println("描述4=" + order.getDes());
}
}
运行结果:
内部使用了递归调用,第二次加入牛奶时,此时order时Milk,order中的drink存放的是第一次加入的milk,第一次加入的milk中的drink是LongBlack
装饰者模式在IO中的使用
IO的UML图:
InputStream是一个顶层的接口,文章开头就说,装饰器模式是继承关系的一种替代方案,看一下为什么:
- InputStream假设这里写了两个实现类,FileInputStream,ObjectInputStream分别表示文件字节输入流,对象字节输入流
- 现在我要给这两个输入流加入一点缓冲功能以提高输入流效率,使用继承的方式,那么就写一个BufferedInputStream,继承FileInputStream,ObjectInputStream,给它们加功能
- 现在我有另外一个需求,需要给这两个输入流加入一点网络功能,那么就写一个SocketInputStream,继承继承FileInputStream,ObjectInputStream,给它们加功能
这样就导致两个问题:
- 因为我要给哪个类加功能就必须继承它,比如我要给FileInputStream,ObjectInputStream加上缓冲功能、网络功能就得扩展出2*2=4个类,更多的以此类推,这样势必导致类数量不断膨胀
- 代码无法复用,给FileInputStream,ObjectInputStream加入缓冲功能,本身代码应该是一样的,现在却必须继承完毕后把一样的代码重写一遍,多此一举,代码修改的时候必须修改多个地方,可维护性很差
所以,这个的时候我们就想到了一种解决方案:
- 在要扩展的类比如BufferedInputStream中持有一个InputStream的引用,在BufferedInputStream调用InputStream中的方法,这样扩展的代码就可以复用起来
- 将BufferedInputStream作为InputStream的子类,这样客户端只知道我用的是InputStream而不需要关心具体实现,可以在客户端不知情的情况下,扩展InputStream的功能,加上缓冲功能
这就是装饰器模式简单的由来,一切都是为了解决实际问题而诞生。下一步,根据UML图,我们来划分一下装饰器模式的角色。
IO的装饰器角色:
- InputStream是抽象类,类似上面例子中的Drink
- ByteArrayInputStream、FileInputStream、ObjectInputStream、PipedInputStream都是具体构建角色,是InputStream的子类,类似之前的具体的单点咖啡如LongBlack、ShortBlack,是被装饰对象
如FileInputStream的声明
public
class FileInputStream extends InputStream
{
- FilterInputStream无疑就是一个装饰角色,因为FilterInputStream实现了InputStream内的所有抽象方法并且持有一个InputStream的引用,就如同之前的Decorator装饰者。
public
class FilterInputStream extends InputStream {
/**
* The input stream to be filtered.
*/
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
- 具体装饰角色就是InflaterInputStream、BufferedInputStream、DataInputStream;类似之前的Milk、Soy
IO流举例:
public static void main(String[] args) throws Exception
{
File file = new File("D:/aaa.txt");
InputStream in0 = new FileInputStream(file);
InputStream in1 = new BufferedInputStream(new FileInputStream(file));
InputStream in2 = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
}
我们这里实例化出了三个InputStream的实现类:
- in0这个引用指向的是new出来的FileInputStream,这里简单构造出了一个文件字节输入流
- in1这个引用指向的是new出来的BufferedInputStream,它给FileInputStream增加了缓冲功能,使得FileInputStream读取文件的内容保存在内存中,以提高读取的功能
- in2这个引用指向的是new出来的DataInputStream,它也给FileInputStream增加了功能,因为它有DataInputStream和BufferedInputStream两个附加的功能
同理,我要给ByteArrayInputStream、ObjectInputStream增加功能,也可以使用类似的方法,整个过程中,最重要的是要理解几个问题:
- 哪些是具体构建角色、哪些是具体装饰角色,尤其是后者,区分的关键就是,角色中是否持有顶层接口的引用
- 每个具体装饰角色有什么作用,因为只有知道每个具体装饰角色有什么作用后,才可以知道要装饰某个功能需要用哪个具体装饰角色
- 使用构造方法的方式将类进行组合,给具体构建角色加入新的功能
装饰者模式的优缺点:
优点
- 装饰器模式与继承关系的目的都是要扩展对象的功能,但是装饰器模式可以提供比继承更多的灵活性。装饰器模式允许系统动态决定贴上一个需要的装饰,或者除掉一个不需要的装饰。继承关系是不同,继承关系是静态的,它在系统运行前就决定了
- 通过使用不同的具体装饰器以及这些装饰类的排列组合,设计师可以创造出很多不同的行为组合
缺点
- 由于使用装饰器模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是另一方面,由于使用装饰器模式会产生比使用继承关系更多的对象,更多的对象会使得查错变得困难,特别是这些对象看上去都很像。
装饰者模式与静态代理模式的区别:
我觉得其实他们可以看成是一样的,但是,其他的地方都说是代理模式是控制对象的访问,装饰者是对被装饰对象进行功能的增强。我觉得其实都一样,装饰者也可以实现静态代理的功能。
可以看看这篇:装饰者和静态代理区别