规律坚持
1. 概念
- 指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式
- 装饰者模式又名包装(Wrapper)模式*(看源码的小伙伴以后看到Wrapper这个后缀词一定要往装饰者模式上想呀)*。装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案
- 装饰者模式动态地将责任附加到对象身上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案
- 装饰者模式完全遵循开闭原则
装饰者模式和继承的比较:
- 继承是编译时静态决定的,所有子类会继承到相同的行为。用户不能控制增加行为的方式和时机
- 装饰者是运行时扩展的,将一个类的对象嵌入到另外个类中,是动态灵活的,有利于扩展。尤其是在软件开发阶段利于维护
- 装饰者模式并不是不需要继承,装饰者利用继承获得类型匹配,而不是利用继承获得了行为,行为是通过组合而不是继承得到的
- 装饰者模式和继承的作用都是去扩展类的功能
2. 结构
装饰模式主要包含以下角色:
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(Concrete Decorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任
装饰者模式的UML图如下所示:
3. 例子
具体的程序,我同步更新到Github上了,一直都有更新,喜欢可以fork同步
这次的例子是根据老友记中的人物来的,还是有点趣味性的
我们希望最终可以输出涂口红和眼影的Rachel,穿衣服的Joey,涂口红穿衣服的Monica,,,等等
继承
如果我们用继承来实现的话,很容易想到,用一个父类Friends
,其里面有口红,眼影,衣服等布尔类型值,然后让Rachel,Joey等六个人去继承Friends就可以。我们可以画出如下类图
-
对于Friends类来说
public class Friends { private boolean eyeShadow; private boolean clothes; private boolean lipstick; void doSomething() { if (isEyeShadow()) { System.out.print("画了眼影"); } if (isClothes()) { System.out.print("穿了衣服"); } if (isLipstick()) { System.out.print("涂了口红"); } } private boolean isEyeShadow() { return eyeShadow; } void setEyeShadow(boolean eyeShadow) { this.eyeShadow = eyeShadow; } private boolean isClothes() { return clothes; } public void setClothes(boolean clothes) { this.clothes = clothes; } private boolean isLipstick() { return lipstick; } void setLipstick(boolean lipstick) { this.lipstick = lipstick; } }
-
Joey, Rose, Rachel, Monica等只需要继承Friends就好
public class Joey extends Friends { @Override void doSomething() { System.out.print("Joey"); super.doSomething(); } } public class Rachel extends Friends { @Override void doSomething() { System.out.print("Rachel"); super.doSomething(); } }
-
当我们调用时:
public static void run() { Friends rachel = new Rachel(); rachel.setEyeShadow(true); rachel.setLipstick(true); rachel.doSomething(); }
Rachel画了眼影涂了口红
但是用继承的话,就像我们之前说的,继承是一种耦合度极高的方式,我们试想这么几种情况:
- 在
Friends
类中我们定义了几个私有布尔变量包括clothes
,eysShadow
等,事实上,根据实际情况,我们会添加更多,如shoes
等等,当我们要改变时,我们就必须要修改已经闭合的代码,这违背了开闭原则 - 同时,当出现新变量的时候,我们还要修改
Friends
的doSomething()
,这显得更加糟糕 - 其实我们已经发现,
Joey
不用画eyeShadow
,但是eyeShadow
已经存在于Friends
中了,并且因为Joey
继承了Friends
,Joey
类中也会有eyeShadow
,这显然不是我们希望看到的,这其实说明了继承的另外一个缺点,太过臃肿,灵活度不高 - 还有,我们发挥脑洞,如果Rachel心血来潮要涂两次口红怎么办?我们的Friends中只有一次口红!
继承的种种缺点就引出了下一步我解决方法:
装饰者模式
我们让Friends
作为Component
,让Joey
,Rachel
等作为ConcreteComponent
,让MakeUp
作为Decorator
,让EyeShadow
,LipStick
等作为ConcreteDecorator
,这就实现了装饰者模式
类图如下(PS,比较下上面装饰者模式的结构):
-
此时的
Friends
已经成为了一个接口public interface Friends { void doSomething(); }
-
Rachel
,Joey
等是Friends的具体组件public class Joey implements Friends { @Override public void doSomething() { System.out.print("Joey"); } } public class Rachel implements Friends { @Override public void doSomething() { System.out.print("Rachel"); } }
-
MakeUp
是Friends的装饰者public class MakeUp implements Friends { Friends friends; MakeUp(Friends friends) { this.friends = friends; } @Override public void doSomething() { friends.doSomething(); } }
-
Clothes
,EyeShadow
等是装饰者的具体实现public class Clothes extends MakeUp { public Clothes(Friends friends) { super(friends); } @Override public void doSomething() { friends.doSomething(); System.out.print("穿了衣服"); } } public class EyeShadow extends MakeUp { public EyeShadow(Friends friends) { super(friends); } @Override public void doSomething() { friends.doSomething(); System.out.print("画了眼影"); } } public class Lipstick extends MakeUp { public Lipstick(Friends friends) { super(friends); } @Override public void doSomething() { super.doSomething(); System.out.print("涂了口红"); } }
-
客户端调用为:
public static void runDecorator() { Friends rachel = new Clothes(new Lipstick(new EyeShadow(new Rachel()))); rachel.doSomething(); }
Rachel画了眼影涂了口红穿了衣服
相信大家都看出来了,所谓的装饰者模式,其实是一种递归
4. 应用
java.io
第一个首推的就是java.io,这里面的API多的要死,刚开始学习的时候把我折磨的不轻,要是早知道用的装饰者模式我也不会那么♂了555
输入流的UML(输出流/Reader/Writer类似)
我们可以看到:
Component
:InputStream
ConcreteComponent
:FileInputStream
,StringBufferInputStream
,StringBufferInputStream
Decorator
:FilterInputStream
ConcreteDecorator
:DataInputStream
,BufferedInputStream
,PushbcakInputStream
,LineNumberInputStream
我们可以基于java的I/O自己写一个装饰器(作用是把文件中的大写变为小写):
public class LowerCaseInputStream extends FilterInputStream {
LowerCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int i = super.read();
return i == -1 ? i : Character.toLowerCase((char)i);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int i = super.read(b, off, len);
for (int j = off; j < off + i; j++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return i;
}
}
启动
public class InputTest {
public static void main(String[] args) {
int c ;
try(InputStream is = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")))){
while ((c = is.read()) >= 0) {
System.out.print((char) c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
javax.swing
我们可以通过装饰模式动态给一些构件增加新的行为或改善其外观显示
如:JList本身不支持直接滚动,没有滚动条,要创建可以滚动的List,我们可以用如下代码
JScrolPane jsp = new JScrollPane(new JList());
5. 扩展
装饰者模式和工厂模式的组合
因为装饰者模式意味着我们要管理更多的类,所以通常情况下,我们一般会将装饰者模式和工厂模式组合起来用。通过工厂模式的控制翻转来几种管理装饰者模式中的装饰者
对装饰者模式的扩展
装饰模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况:
- 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件
- 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并
6. 优劣
装饰者模式的优点
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式以提供比继承更多的灵活性
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得 到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的 具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
装饰者模式的缺点
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继 承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐
7. 面试
面试官:谈一谈你对装饰者模式的理解
- 说出装饰者模式的结构
- 装饰者模式的优化,即和工厂模式结合起来
- 装饰者模式和适配器模式的区别
- 装饰者模式和继承的比较
- 装饰者模式的应用