装饰器模式
源码:https://github.com/GiraffePeng/design-patterns
1、定义
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
通过这句话,我们可以知道装饰器模式不允许修改被装饰的类,如果要附加职责,需要进行拓展,符合开闭原则。
然后按照单一职责原则,某一个对象只专注于干一件事,而如果要扩展其职能的话,不如想办法分离出一个类来“包装”这个对象,而这个扩展出的类则专注于实现扩展功能
2、应用场景
- 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
- 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
- 当对象的功能要求可以动态地添加,也可以再动态地撤销时。
3、模式结构
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
模式的结构
装饰模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
4、应用实例+与其他容器混淆模式对比
4.1、登录实例(继承与装饰器的区别)
我们拿登录来说,假如一个业务需求,需要实现普通的登录,第三方QQ登录,第三方微信登录。
我们通常会怎么设计,我们会先创建一个登录接口,然后根据多态创建其不同的实现类来对应每个登录场景。
按照上面的说法我们来创建一个登录接口。
//声明登录接口
public interface LoginService {
public void login();
}
根据不同的场景,我们创建不同的实现类,分别对应普通的登录,第三方QQ登录,第三方微信登录。
//普通登录
public class LoginBaseServiceImpl implements LoginService{
@Override
public void login() {
System.out.println("登录方法");
}
}
//微信登录
public class LoginWechatServiceImpl implements LoginService{
@Override
public void login() {
System.out.println("微信登录方法");
}
}
//微信登录
public class LoginWechatServiceImpl implements LoginService{
@Override
public void login() {
System.out.println("微信登录方法");
}
}
接下来我们有个新需求说,要在登录功能中加入一些新的逻辑处理或者新的结合登录的前置功能。‘
根据开闭原则,对修改关闭对拓展开放,如果在不改动原有代码的基础上,我们如何去实现这个功能,应该怎么来写?
通常我们会想到继承方式,使用继承来拓展父类的方法。
4.1.1、继承实现父类的拓展
既然要在所有的登录功能前做些处理,我们需要对每个场景下的登录实现来创建其子类来实现。
创建各自的子类,分别对应普通的登录,第三方QQ登录,第三方微信登录。
//普通登录子类
public class LoginPrefixServiceImpl extends LoginBaseServiceImpl{
@Override
public void login() {
doSomething();
super.login();
}
public void doSomething() {
System.out.println("拓展一些方法");
}
}
//QQ登录子类
public class LoginPrefixQQServiceImpl extends LoginQQServiceImpl{
@Override
public void login() {
doSomething();
super.login();
}
public void doSomething() {
System.out.println("拓展一些方法");
}
}
//微信登录子类
public class LoginPrefixWechatServiceImpl extends LoginWechatServiceImpl{
@Override
public void login() {
doSomething();
super.login();
}
public void doSomething() {
System.out.println("拓展一些方法");
}
}
这时的类的结构就变成如下图所示:
ok,我们完成了这次需求的改动。随着业务的发展,又有新的需求来了,需要在登录的功能后加入逻辑处理或者结合登陆的后置功能,我们用继承怎么来写? 会按照上述的方式再去继承父类,变成下面这个样子。
那么好,需求又说了,我要结合以前的登录功能,在登录的前后都要加入相应的逻辑处理,且处理方式与之前的一致。即 新需求 = 前面实现的登录前逻辑处理 + 登录 + 前面实现的登录后逻辑处理。
这时候我们用继承怎么来写,难道要继续创建每个场景下的子类吗。这样随着需求的变化,我们会用N多个子类,很不灵活。我们能不能把前置逻辑处理以及后置逻辑处理抽取为单独的模块,供这些登录场景使用呢?意思就是在登录场景中修饰它,为它动态的增加职责,而不是静态的通过继承来确认相互之间的关系。
这时就引出的装饰器模式。下面我们来用装饰器来实现上述的需求场景。
4.1.2 装饰器实现类的拓展
根据上述的场景,抽象构件角色(LoginService)和具体构建角色(LoginBaseServiceImpl、LoginQQServiceImpl、LoginWechatServiceImpl)已经具备了,利用装饰器模式的话,我们还需要抽象装饰角色以及具体装饰角色。下面我们来创建抽象装饰角色.
//装饰器 抽象装饰角色
public abstract class AbstractLoginServiceImpl implements LoginService{
private LoginService loginService;
public AbstractLoginServiceImpl(LoginService loginService) {
super();
this.loginService = loginService;
}
public void login() {
loginService.login();
}
}
根据上述需求,我们了解到,要在登陆的前后分别添加新的职责,我们按照抽象装饰角色来实现多个场景。
//前置拓展类 具体抽象装饰角色
public class DoPrefixLoginServiceImpl extends AbstractLoginServiceImpl{
public DoPrefixLoginServiceImpl(LoginService loginService) {
super(loginService);
}
public void login() {
doSomething();
super.login();
}
public void doSomething() {
System.out.println("在方法前拓展一些方法");
}
}
//后置拓展类 具体抽象装饰角色
public class DoSuffixLoginServiceImpl extends AbstractLoginServiceImpl{
public DoSuffixLoginServiceImpl(LoginService loginService) {
super(loginService);
}
public void login() {
super.login();
doSomething();
}
public void doSomething() {
System.out.println("在方法后拓展一些方法");
}
}
这个时候我们来看下这个类图
创建测试类
public class Test {
public static void main(String[] args) {
LoginService loginService = new LoginQQServiceImpl();
loginService.login();
System.out.println("---------");
//在方法前拓展内容
LoginService doPrefixLoginService = new DoPrefixLoginServiceImpl(loginService);
doPrefixLoginService.login();
System.out.println("---------");
//想在方法后拓展内容
LoginService doSuffixLoginService = new DoSuffixLoginServiceImpl(loginService);
doSuffixLoginService.login();
System.out.println("---------");
//即想在方法前拓展内容 ,又想在方法后拓展内容
LoginService doSuffixAndPrefixLoginService = new DoSuffixLoginServiceImpl(doPrefixLoginService);
doSuffixAndPrefixLoginService.login();
}
}
打印结果:
QQ登陆方法
---------
在方法前拓展一些方法
QQ登陆方法
---------
QQ登陆方法
在方法后拓展一些方法
---------
在方法前拓展一些方法
QQ登陆方法
在方法后拓展一些方法
这个时候不管需求如何变动,我们都可以通过抽象装饰器为基础,来动态的定义新的子类来满足各个场景下的需求。
4.1.3 继承与装饰器总结
- 装饰器模式比继承要灵活。避免了继承体系臃肿
- 装饰器模式降低了类于类之间的关系。
- 你要说用装饰器实现的功能,继承能否实现,我只能说能,但是在代码的结构层次上来说,装饰器模式比继承灵活了很多。
- 装饰模式与继承关系的目的都是要拓展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者“除掉”一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了
- 看上面的测试类可以知道:通过不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出更多不同行为的组合。
4.2、装饰器与适配器的区别
这里不做过多实例代码对比,可参考上一篇文章代码实例做对比:《八、适配器模式(类适配器、对象适配器、双向适配器)[AdapterPattern]》
装饰者和适配器模式都是包装模式(Wrapper Pattern),装饰者也是一种特殊的代理模式.
装饰者模式 | 适配器模式 | |
---|---|---|
形式 | 是一种非常特别的适配器模式 | 没有层级关系,装饰器模式有层级关系 |
定义 | 装饰者和被装饰者都实现同一个接口,主要目的是为了拓展之后依旧保持OOP关系 | 适配器和被适配者没有必然的联系,通常采用继承或代理的形式进行包装 |
关系 | 满足 is-a 的关系 | 满足 has-a 的关系 |
功能 | 注重覆盖、扩展 | 注重兼容、转换 |
设计 | 前置考虑 | 后置考虑 |
4.3、装饰器与代理模式区别
又有小伙伴说了,这装饰器模式与代理模式好像啊,都是在方法的前后增加附加的代码块来完成某个功能的实现。
既然把他们划分为两个设计模式,就当然会有区别。
装饰者模式 | 代理模式 | |
---|---|---|
关注点 | 关注于在一个对象上动态的添加方法 | 关注于控制对对象的访问 |
原有对象的持有方式 | 当我们使用装饰器模 式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 | 用代理模式,代理类可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例 |
注重点 | 注重对对象功能的扩展,它不关心外界如何调用,只注重对对象功能的加强,装饰后还是对象本身 | 注重对对象某一功能的流程把控和辅助。它可以控制对象做某些事,偏重因自己无法完成或自己无需关心,需要他人干涉事件流程,更多的是对对象的控制重心是为了借用对象的功能完成某一流程,而非对象功能如何 |
客户端调用 | A类是原始功能的类, B是装饰模式中对A类的扩展之后的类,使用装饰模式, 用户更关系的是B的功能(包含A的原始功能) | A类是原始功能的类,C是代理模式中对A类的扩展,使用代理模式,用户更关心A的功能。并不关心C的功能 |
对于代理类,如何调用对象的某一功能是思考重点,而不需要兼顾对象的所有功能; 对于装饰类,如何扩展对象的某一功能是思考重点,同时也需要兼顾对象的其它功能,因为再怎么装饰,本质也是对象本身,要担负起对象应有的职责。
尽管他们在写法上会有很多相似的地方,但所表达的意思缺失完全不同的。大家只要记住一句话:代理模式是控制对象的访问,装饰器是增强对象的功能。
针对用户或者客户端来说
- 调用装饰器模式,它需要知道原有对象的功能以及装饰器类相应增强的功能,并且装饰器类里有原有对象的所有方法。
- 调用代理模式,它可能并不知道原有对象本身,它只知道代理类的功能,并且代理类中只持有原有对象的部分方法来进行代理加强。
5、装饰器在源码中的应用
由于JAVA I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一种类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最小。因此装饰模式是JAVA I/O库的基本模式。
以InputStream为例,看下类图
5.1、示例
public class StreamTest {
public static void main(String[] args) {
DataInputStream dis = null;
try {
dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("23.txt")
)
);
byte[] bytes = new byte[dis.available()];
dis.read(bytes);
String content = new String(bytes);
System.out.println(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream对象处理后,再将处理后的对象传递给DataInputStream对象进行处理。这个过程,其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。
6、优缺点
装饰(Decorator)模式的主要优点有:
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合
其主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。