
聊一聊装饰者模式
是你,还是你,一切都有你!—— 装饰者模式
一、概述
装饰者模式(Decorator Pattern)允许向一个现有的对象扩展新的功能,同时不改变其结构。主要解决直接继承下因功能的不断横向扩展导致子类膨胀的问题,无需考虑子类的维护。
装饰者模式有4种角色:
- 抽象构件角色(Component):具体构件类和抽象装饰者类的共同父类。
- 具体构件角色(ConcreteComponent):抽象构件的子类,装饰者类可以给它增加额外的职责。
- 装饰角色(Decorator):抽象构件的子类,具体装饰类的父类,用于给具体构件增加职责,但在子类中实现。
- 具体装饰角色(ConcreteDecorator):具体装饰类,定义了一些新的行为,向构件类添加新的特性。
二、入门案例
2.1、类图
2.2、基础类介绍
当然,如果需要扩展更多功能的话,可以再定义其他的ConcreteDecorator类,实现其他的扩展功能。如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
三、应用场景
如风之前在一家保险公司干过一段时间。其中保险业务员也会在自家产品注册账号,进行推销。不过在这之前,他们需要经过培训,导入一张展业资格证书。然后再去推销保险产品供用户下单,自己则通过推销产生的业绩,参与分润,拿对应的佣金。
对于上面导证书这个场景,实际上是会根据不同的保险产品,导入不同的证书的。并且证书的类型也不同,对应的解析、校验、执行的业务场景都是不同的。如何去实现呢?当然if-else确实也是一种不错的选择。下面放一段伪代码
从上面的伪代码看到,所有的业务逻辑是在一起处理的,通过productCode去处理对应产品的相关逻辑。这么一看,好像也没毛病,但是还是被技术大佬给否决了。好吧,如风决定重写。运用装饰者模式,重新处理下了下这段代码。1、一切再从注解出发,自定义Decorate
注解,这里定义2个属性,scene和type
- scene:标记具体的业务场景
- type:表示在该种业务场景下,定义一种具体的装饰器类
2、抽象构件接口,BaseHandler
,这个是必须滴
3、抽象装饰器类,AbstractHandler
,持有一个被装饰类的引用,这个引用具体在运行时被指定
4、具体的装饰器类AProductServiceDecorate
,主要负责处理“导师证书”这个业务场景下,A产品相关的导入逻辑,并且标记了自定义注解Decorate
,表示该类是装饰器类。主要负责对A产品证书导入之前逻辑的增强,我们这里称之为“装饰”。
当然,还是其他装饰类,BProductServiceDecorate
,CProductServiceDecorate
等等,负责装饰其他产品,这里就不举例了。
5、当然还有管理装饰器类的装饰器类管理器DecorateManager,内部维护一个map,负责存放具体的装饰器类
6、用了springboot,当然需要将这个管理器交给spring的bean容器去管理,需要创建一个配置类DecorateAutoConfiguration
7、被装饰的service类,CertificateService
,只需要关注自己的核心逻辑就可以
8、在原来的controller中,注入管理器类DecorateManager
去调用,以及service,也就是被装饰的类。首先拿到装饰器,然后再通过setService方法,传入被装饰的service。也就是具体装饰什么类,需要在运行时才确定。
下面模拟下代理人导入证书的流程,当选择A产品,productCode传A过来,后端的处理流程。
- 对于A产品下,证书的解析,A产品传的是excel
- 然后数据校验,这个产品下,特有的数据校验
- 最后是核心的业绩重算,只有A产品才会有这个逻辑
当选择B产品,productCode传A过来,后端的处理流程。
- 对于B产品下,证书的解析,A产品传的是pdf
- 然后数据校验,跟A产品也不同,多了xxx步骤
- 核心是代理人的晋升处理,这部分是B产品独有的
最后说一句,既然都用springboot了,这块可以写一个starter,做一个公用的装饰器模式。如果哪个服务需要用到,依赖这个装饰器的starter,然后标记Decorate
注解,定义对应的scene和type属性,就可以直接使用了。
四、源码中运用
4.1、JDK源码中的运用
来看下IO流中,InputStream
、FilterInputStream
、FileInputStream
、BufferedInputStream
的一段代码
再来看下这几个类的类图
这些类的代码有删改,可以看到BufferedInputStream
中定义了很多属性,这些数据都是为了可缓冲读取来作准备的,看到其有构造方法会传入一个InputStream的实例。实际编码如下
这里觉得很眼熟吧,其实已经运用了装饰模式了。
4.2、mybatis源码中的运用
在mybatis中,有个接口Executor
,顾名思义这个接口是个执行器,它底下有许多实现类,如CachingExecutor
、SimpleExecutor
、BaseExecutor
等等。类图如下:
主要看下CachingExecutor
类,看着很眼熟,很标准的装饰器。其中该类中的update是装饰方法,在调用真正update方法之前,会执行刷新本地缓存的方法,对原来的update做增强和扩展。
再来看下BaseExecutor
类,这里有一个update方法,这个是原本的被装饰的update方法。然后再看这个原本的update方法,它调用的doUpdate方法是个抽象方法,用protected修饰。咦,这不就是模板方法么,关于模板方法模式,这里就不展开赘述了。
五、总结
优点
- 通过组合而非继承的方式,动态地扩展一个对象的功能,在运行时可以选择不同的装饰器从而实现不同的功能。
- 有效的避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
- 具体组件类与具体装饰类可以独立变化,用户可以根据需要新增具体组件类跟装饰类,在使用时在对其进行组合,原有代码无须改变,符合"开闭原则"。
缺点
- 这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。
- 装饰模式会导致设计中出现许多小类 (I/O 类中就是这样),如果过度使用,会使程序变得很复杂。
- 装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。
六、参考源码
文章转载自公众号:知了一笑
