修饰模式理解起来还是相对比较简单的,就增加功能来讲,装饰模式可以比设计为子类的方式更为灵活,能让对象专注于自己核心职责,其余的事情交给修饰类来负责。
装饰模式属于七个结构型模式之一,其余六个是:
- 适配器模式 – Adapter Pattern
- 桥接模式 – Bridge Pattern
- 组合模式 – Composite Pattern
- 外观模式 – Facade Pattern
- 享元模式 – Flyweight Pattern
- 代理模式 – Proxy Pattern
定义
装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说, 装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
装饰模式结构及对象角色
- Component(抽象构件):就是核心业务类,被修饰的抽象层,里面声明自己的核心操作,主要职责。
-
ConcreteComponent(具体构件):核心业务类职责的主要实现。
-
Decorator(抽象装饰类):Component 的子类或者实现 Component 接口,这里主要封装对 Component 核心职责方法的调用。
-
ConcreteDecorator(具体装饰类):Decorator 的子类,负责具体的装饰以及 Component 核心方法的调用。
因为装饰模式的思想相对来讲比较简单,所以我们直接来看代码。先来设计一个面板抽象类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package learn.design.patterns.decorator; /** * 面板抽象类 * @author: glorze.com * @since: 2020/7/5 17:24 */ public abstract class Panel { /** * 展示面板 * @Title: display * @return void */ abstract void display(); /** * 关闭面板 * @Title: display * @return void */ abstract void close(); /** * 最小化面板 * @Title: display * @return void */ abstract void minimize(); } |
实现一个具体的 QQ 面板类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package learn.design.patterns.decorator; /** * QQ 面板实现类 * * @ClassName: QQPanel * @author: 高老四博客 * @since: 2020/7/5 16:57 */ public class QQPanel extends Panel { @Override public void display() { System.out.println("展示 QQ 面板!"); } @Override public void close() { System.out.println("关闭 QQ 面板!"); } @Override public void minimize() { System.out.println("最小化 QQ 面板!"); } } |
接着设计一个装饰的抽象类,也是继承 Panel 面板抽象类,然后在这个装饰抽象类里面将面板的核心职责引入,也就是在这里面对面板的方法进行引用。这是装饰模式的核心,需要好好领悟一下这个抽象类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package learn.design.patterns.decorator; /** * 面板修饰抽象类 * @author: glorze.com * @since: 2020/7/5 17:28 */ public abstract class PanelDecorator extends Panel { private Panel panel; public PanelDecorator(Panel panel) { this.panel = panel; } @Override public void display() { panel.display(); } @Override public void close() { panel.close(); } @Override public void minimize() { panel.minimize(); } } |
最后,对于面板的核心职责方法的调用以及对于面板的额外修饰都是在修饰抽象类的子类中去实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package learn.design.patterns.decorator; /** * 具体的面板装饰实现类 * * @ClassName: SkinPanelDecorator * @author: Glorze * @since: 2020/7/5 17:04 */ public class SkinPanelDecorator extends PanelDecorator { public SkinPanelDecorator(Panel panel) { super(panel); } @Override public void display() { super.display(); this.changeSkin(); } /** * 更换 QQ 皮肤 * @Title: changeSkin * @return void */ public void changeSkin() { System.out.println("为当前 QQ 面板更换「凉爽夏日」皮肤!"); } public static void main(String[] args) { Panel QQPanel = new QQPanel(); Panel decoratorPanel = new SkinPanelDecorator(QQPanel); decoratorPanel.display(); } } |
相信以上的代码流程能让我们有一个直观的了解,对于装饰模式的思想我们也就理解了。
装饰模式优缺点总结以及适用场景
优点
-
对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
-
可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的 具体装饰类,从而实现不同的行为。
-
可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合, 可以创造出很多不同行为的组合,得到功能更为强大的对象。
-
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装 饰类,原有类库代码无须改变,符合「开放-封闭原则」。
缺点
-
使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接 的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多 的系统资源,在一定程序上影响程序的性能。
-
装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出 错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装 饰模式。
那么在 Java 中不能继承的情况都有哪些呢?第一类是系统中存在大量独立的扩展,为支持每 一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因 为类已定义为不能被继承,比如 final 修饰的类。
装饰模式相比较于继承有哪些优势?
从上述中我们能看出,装饰者模式解决的问题我们都能通过继承来解决,我们可以创建子类并在子类中添加新功能实现扩展。这种方法是静态的,用户不能控制增加行为的方式和时机。而且有些情况下继承是不可行的,例如已有组件是被 final 关键字修饰的类。另外,如果待添加的新功能存在多种组合,使用继承方式可能会导致大量子类的出现。例如,有 4 个待添加的新功能,系统需要动态使用任意多个功能的组合,则需要添加 15 个子类才能满足全部需求。
解决上述问题,装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。在实践中,我们应该尽量使用组合的方式来扩展系统的功能,而非使用继承的方式。通过装饰器模式的介绍,可以帮助读者更好地理解设计模式中常见的一句话:组合优于继承。
—- 来自于《MyBatis 技术内幕》
装饰模式在 Spring 中的使用
本段文章主要参考书籍《Spring 5核心原理与30个类手写实战》,侵删。
装饰者模式(Decorator Pattern)是指在不改变原有对象的基础上,将功能附加到对象上,提供了比继承更有弹性的方案(扩展原有对象的功能),属于结构型模式。装饰者模式在生活中的应用也比较多,如给煎饼加鸡蛋、给蛋糕加一些水果、给房子装修等,都是在为对象扩展一些额外的职责。装饰者模式适用于以下场景:
- 扩展一个类的功能或给一个类添加附加职责。
- 动态给一个对象添加功能,这些功能可以再动态地撤销。
装饰者模式广泛引用于 JDK 源码和 Spring 源码当中,比如在 JDK 中,BufferedResder、InputStream 等就是装饰者,比如我们如果继承 FilterInputStream,那实现的就是一个具体的装饰者类。
在 Spring 中,装饰者模式的应用几乎到处都是,如果在源码阅读的过程中发现类是以「Decorator」结尾的那都是装饰者类,另外就是有一些 Wrapper 结尾的类也可能应用的是装饰者模式。举几个典型的例子供参阅。
BeanDefinitionDecorator
我们知道作为 beans 的第一级子标签,这种使用方式需要 BeanDefinitionParser 来处理,而作为 bean 标签的子标签,作用于 property 属性之上,这种使用方式就是对 bean 的装饰过程,需要 BeanDefinitionDecorator 来处理。
TransactionAwareCacheDecorator
TransactionAwareCacheDecorator 类是对缓存 Cache 接口的装饰,声明了 targetCache 成员属性负责对缓存的核心调用,然后通过 put/evict/clear 等操作与 Spring 管理的事务同步,仅在成功的事务的 afterCommit 阶段执行实际的缓存 put/evict/clear 操作。如果没有事务是 active 的,将立即执行 put/evict/clear 操作。
SpringMVC 中的 HttpHeadResponseDecorator
HttpHeadResponseDecorator 装饰类,内部直接封装了 ServerHttpResponse 调用其核心方法,最后通过流式 API 进行返回。
Spring WEB Servlet 包中的 ServletRequestWrapper
ServletRequestWrapper 实现对 ServletRequest 接口的包装,主要对 HTTP 功能进行增强。
另外在我们常用的 Mybatis 框架中也是专门使用了装饰者模式,类似于 Spring 中的事务缓存,在 MyBatis 中装饰者模式也运用于对 Cache 接口的装饰。Mybatis 作为一个强大的持久层框架,缓存是其必不可少的功能之一。Mybatis 中的缓存是两层结构的,分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口的实现。
在 MyBatis 源码中,是专门有一个「org.apache.ibatis.cache.decorators」包的,里面封装了各种各样的装饰类对 Cache 接口进行修饰,而 Cache 接口只有一个最基本的实现类「PerpetualCache」,其余都是对 Cache 接口的装饰类,实现阻塞、同步、日志、LRU、FIFO 等 特性的修饰。
- BlockingCache 阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据
- FifoCache FifoCache 是先入先出的装饰器,当向缓存添加数据时,如果缓存项的个数己经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。
- LruCache 按照近期最少使用算法进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。底层使用 LinkedHashMap。
- SoftCache & WeakCache 在 SoftCache 中,最近使用的一部分缓存项不会被 GC 回收,这就是通过将其 value 添加到 hardLinksToAvoidGarbageCollection 集合中实现的(即有强引用指向其 value)
- ScheduledCache 周期性清理缓存的装饰器
- LoggingCache 在 Cache 的基础上提供了日志功能
- SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能
- SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象进行序列化,并将序列化后的 byte 数组作为 value 存入缓存
在这些修饰类中,你都会看到一个 「private final Cache delegate;」的底层 Cache 对象声明,从而对 Cache 接口进行修饰。
相关阅读
- 《浅析设计模式第十七章之适配器模式》
- 《浅析设计模式第十二章之外观模式》
- 《浅析设计模式第四章之开放-封闭原则》
- 《Java中I/O输入输出流之对象序列化浅析》
- 《IntelliJ IDEA 导入 Spring 源码教程》
- 《IntelliJ IDEA 导入 JDK 1.8 源码教程》
- 《MyBatis 常见笔试题面试题整理》
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请捐赠盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额(点击「给你买杜蕾斯」),也可以加入本站封闭式交流论坛「DownHub」开启新世界的大门,老四这里抱拳谢谢诸位了。捐赠时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的捐赠钱财也会被用于小站的服务器运维上面,再次抱拳感谢。