共计 10344 个字符,预计需要花费 26 分钟才能阅读完成。
是你,还是你,所有都有你!—— 装璜者模式
一、概述
装璜者模式(Decorator Pattern)容许向一个现有的对象扩大新的性能,同时不扭转其构造。次要解决间接继承下因性能的一直横向扩大导致 子类收缩 的问题,无需思考子类的保护。
装璜者模式有 4 种角色:
- 形象构件角色(Component):具体构件类和形象装璜者类的独特父类。
- 具体构件角色(ConcreteComponent):形象构件的子类,装璜者类能够给它减少额定的职责。
- 装璜角色(Decorator):形象构件的子类,具体装璜类的父类,用于给具体构件减少职责,但在子类中实现。
- 具体装璜角色(ConcreteDecorator):具体装璜类,定义了一些新的行为,向构件类增加新的个性。
二、入门案例
2.1、类图
2.2、根底类介绍
// 形象构件角色 | |
public interface Component {void doSomeThing(); | |
} | |
// 具体构件角色 | |
public class ConcreteComponent implements Component { | |
@Override | |
public void doSomeThing() {System.out.println("解决业务逻辑"); | |
} | |
} | |
// 装璜者类 | |
public abstract class Decorator implements Component { | |
private Component component; | |
public Decorator(Component component) {this.component = component;} | |
@Override | |
public void doSomeThing() { | |
// 调用解决业务逻辑 | |
component.doSomeThing();} | |
} | |
// 具体装璜类 | |
public class ConcreteDecorator extends Decorator {public ConcreteDecorator(Component component) {super(component); | |
} | |
@Override | |
public void doSomeThing() {System.out.println("业务逻辑性能扩大"); | |
super.doSomeThing();} | |
} |
当然,如果须要扩大更多功能的话,能够再定义其余的 ConcreteDecorator 类,实现其余的扩大性能。
如果只有一个 ConcreteDecorator 类,那么就没有必要建设一个独自的 Decorator 类,而能够把 Decorator 和 ConcreteDecorator 的责任合并成一个类。
三、利用场景
- 如风之前在一家保险公司干过一段时间。其中保险业务员也会在自家产品注册账号,进行采购。不过在这之前,他们须要经过培训,导入一张展业资格证书。而后再去采购保险产品供用户下单,本人则通过采购产生的业绩,参加分润,拿对应的佣金。
- 对于下面导证书这个场景,实际上是会依据不同的保险产品,导入不同的证书的。并且证书的类型也不同,对应的解析、校验、执行的业务场景都是不同的。如何去实现呢?当然 if-else 的确也是一种不错的抉择。上面放一段伪代码
/** | |
* @author 往事如风 | |
* @version 1.0 | |
* @date 2022/11/17 11:32 | |
* @description | |
*/ | |
@RestController | |
@RequestMapping("/certificate") | |
public class CertificateController { | |
@Resource | |
private CommonCertificateService certificateService; | |
@PostMapping("/import") | |
public Result<Integer> importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {return Result.success(certificateService.importCertificate(file, productCode)); | |
} | |
} | |
/** | |
* @author 往事如风 | |
* @version 1.0 | |
* @date 2022/11/17 13:25 | |
* @description | |
*/ | |
@Service | |
public class CommonCertificateService {public Integer importCertificate(MultipartFile file, String productCode) { | |
// 1、参数非空校验 | |
// 2、通过 file 后缀判断 file 类型,反对 excel 和 pdf | |
// 3、解析 file 文件,获取数据,对立封装到定义的 CertificatePojo 类中 | |
// 4、依据产品类型判断导入之前的业务逻辑 | |
if (productCode.equals(DecorateConstants.PRODUCT_A)) { | |
// 从新计算业绩逻辑 | |
// 从新算业绩类型逻辑 | |
// 一坨坨代码去实现.... | |
} | |
else if (productCode.equals(DecorateConstants.PRODUCT_B)) { | |
// 导入证书的代理人本人以及下级身份降职逻辑 | |
// 业绩计算逻辑 | |
// 一坨坨代码去实现... | |
} else if (productCode.equals(DecorateConstants.PRODUCT_C)) { | |
// c 产品下的业务逻辑 | |
// 一坨坨代码去实现... | |
} else { | |
// 默认的解决逻辑 | |
// 一坨坨代码去实现... | |
} | |
// 5、证书数据保留 | |
// 6、代理人信息保留 | |
// 7、相干流水数据保留 | |
// 返回代理人 id | |
Integer agentId = Integer.MAX_VALUE; | |
return agentId; | |
} | |
} |
从下面的伪代码看到,所有的业务逻辑是在一起解决的,通过 productCode 去解决对应产品的相干逻辑。这么一看,如同也没故障,然而还是被技术大佬给否决了。好吧,如风决定重写。使用装璜者模式,重新处理下了下这段代码。
1、所有再从注解登程,自定义 Decorate
注解,这里定义 2 个属性,scene 和 type
- scene:标记具体的业务场景
-
type:示意在该种业务场景下,定义一种具体的装璜器类
/** * @author 往事如风 * @version 1.0 * @date 2022/11/8 17:44 * @description */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Service public @interface Decorate { /** * 具体的业务场景 * @return */ String scene(); /** * 类型: 不同业务场景下,不同的装璜器类型 * @return */ String type();} 2、形象构件接口,
BaseHandler
,这个是必须滴/** * @author 往事如风 * @version 1.0 * @date 2022/11/8 17:07 * @description 形象解决接口 */ public interface BaseHandler<T, R> { /** * 对立的解决办法 * @param t * @return */ R handle(T t); } 3、形象装璜器类,
AbstractHandler
,持有一个被装璜类的援用,这个援用具体在运行时被指定/** * @author 往事如风 * @version 1.0 * @date 2022-11-13 22:10:05 * @desc 形象父类 */ public abstract class AbstractHandler<T, R> implements BaseHandler<T, R> { protected BaseHandler service; public void setService(BaseHandler service) {this.service = service;} } 4、具体的装璜器类
AProductServiceDecorate
,次要负责解决“导师证书”这个业务场景下,A 产品相干的导入逻辑,并且标记了自定义注解Decorate
,示意该类是装璜器类。次要负责对 A 产品证书导入之前逻辑的加强,咱们这里称之为“装璜”。/** * @author 往事如风 * @version 1.0 * @date 2022-11-13 23:11:16 * @desc */ @Decorate(scene = SceneConstants.CERTIFICATE_IMPORT, type = DecorateConstants.PRODUCT_A) public class AProductServiceDecorate extends AbstractHandler<MultipartFile, Integer> { /** * 重写父类解决数据办法 * @param file * @return */ @Override public Integer handle(MultipartFile file) { // 解析 CertificatePojo data = parseData(file); // 校验 check(data); // 业绩计算 calAchievement(data.getMobile()); return (Integer) service.handle(data); } public CertificatePojo parseData(MultipartFile file) { // file, 证书解析 System.out.println("A 产品的证书解析......"); CertificatePojo certificatePojo = new CertificatePojo(); certificatePojo.setMobile("12323"); certificatePojo.setName("张三"); certificatePojo.setMemberNo("req_343242ds"); certificatePojo.setEffectDate("2022-10-31:20:20:10"); return certificatePojo; } /** * 证书数据校验 * @param data */ public void check(CertificatePojo data) { // 数据标准和重复性校验 // ..... System.out.println("A 证书数据校验......"); } /** * 计算业绩信息 */ private void calAchievement(String mobile) {System.out.println("查问用户信息, 手机号:" + mobile); System.out.println("从新计算业绩..."); } } 当然,还是其余装璜类,
BProductServiceDecorate
,CProductServiceDecorate
等等,负责装璜其余产品,这里就不举例了。
5、当然还有治理装璜器类的装璜器类管理器 DecorateManager,外部保护一个 map,负责寄存具体的装璜器类/** * @author 往事如风 * @version 1.0 * @date 2022/11/15 17:18 * @description 装璜管理器 */ public class DecorateManager { /** * 用于寄存装璜器类 */ private Map<String, AbstractHandler> decorateHandleMap = new HashMap<>(); /** * 将具体装璜器类放在 map 中 * * @param handlerList */ public void setDecorateHandler(List<AbstractHandler> handlerList) {for (AbstractHandler h : handlerList) {Decorate annotation = AnnotationUtils.findAnnotation(h.getClass(), Decorate.class); decorateHandleMap.put(createKey(annotation.scene(), annotation.type()), h); } } /** * 返回具体的装璜器类 * * @param type * @return */ public AbstractHandler selectHandler(String scene, String type) {String key = createKey(scene, type); return decorateHandleMap.get(key); } /** * 拼接 map 的 key * @param scene * @param type * @return */ private String createKey(String scene, String type) {return StrUtil.builder().append(scene).append(":").append(type).toString();} } 6、用了 springboot,当然须要将这个管理器交给 spring 的 bean 容器去治理,须要创立一个配置类
DecorateAutoConfiguration
/** * @author 往事如风 * @version 1.0 * @date 2022-11-12 19:22:41 * @desc */ @Configuration public class DecorateAutoConfiguration { @Bean public DecorateManager handleDecorate(List<AbstractHandler> handlers) {DecorateManager manager = new DecorateManager(); manager.setDecorateHandler(handlers); return manager; } } 7、被装璜的 service 类,
CertificateService
,只须要关注本人的外围逻辑就能够/** * @author 往事如风 * @version 1.0 * @date 2022/11/8 17:10 * @description 执行证书导入的 service */ @Service public class CertificateService implements BaseHandler<CertificatePojo, Integer> { /** * 解决导入证书的外围逻辑 service * @param certificate * @return */ @Override public Integer handle(CertificatePojo certificate) {System.out.println("外围业务, 证书数据:" + JSONUtil.toJsonStr(certificate)); // 1、证书数据保留 // 2、代理人信息保留 // 3、相干流水数据保留 // 其余的一些列外围操作 Integer agentId = Integer.MAX_VALUE; // 返回代理人 id return agentId; } } 8、在原来的 controller 中,注入管理器类
DecorateManager
去调用,以及 service,也就是被装璜的类。首先拿到装璜器,而后再通过 setService 办法,传入被装璜的 service。也就是具体装璜什么类,须要在运行时才确定。/** * @author 往事如风 * @version 1.0 * @date 2022-11-13 23:30:37 * @desc */ @RestController public class WebController { @Resource private DecorateManager decorateManager; @Resource private CertificateService certificateService; @PostMapping("/import") public Result importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {AbstractHandler handler = decorateManager.selectHandler(SceneConstants.CERTIFICATE_IMPORT, productCode); if (Objects.isNull(handler)) {return Result.fail(); } handler.setService(certificateService); return Result.success(handler.handle(file)); } } 上面模仿下代理人导入证书的流程,当抉择 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
的一段代码
public abstract class InputStream implements Closeable {public abstract int read() throws IOException; | |
public int read(byte b[], int off, int len) throws IOException {if (b == null) {throw new NullPointerException(); | |
} else if (off < 0 || len < 0 || len > b.length - off) {throw new IndexOutOfBoundsException(); | |
} else if (len == 0) {return 0;} | |
int c = read(); | |
if (c == -1) {return -1;} | |
b[off] = (byte)c; | |
int i = 1; | |
try {for (; i < len ; i++) {c = read(); | |
if (c == -1) {break;} | |
b[off + i] = (byte)c; | |
} | |
} catch (IOException ee) { } | |
return i; | |
} | |
} | |
//-------------------------- | |
public class FilterInputStream extends InputStream {protected FilterInputStream(InputStream in) {this.in = in;} | |
public int read() throws IOException {return in.read(); | |
} | |
} | |
//-------------------------- | |
public class BufferedInputStream extends FilterInputStream {public BufferedInputStream(InputStream in, int size) {super(in); | |
if (size <= 0) {throw new IllegalArgumentException("Buffer size <= 0"); | |
} | |
buf = new byte[size]; | |
} | |
public BufferedInputStream(InputStream in) {this(in, DEFAULT_BUFFER_SIZE); | |
} | |
public int read() throws IOException {return in.read(); | |
} | |
public int read(byte b[], int off, int len) throws IOException {return in.read(b, off, len); | |
} | |
} | |
//-------------------------- | |
public class FileInputStream extends InputStream {public int read(byte b[]) throws IOException {return readBytes(b, 0, b.length); | |
} | |
} | |
再来看下这几个类的类图
这些类的代码有删改,能够看到 BufferedInputStream
中定义了很多属性,这些数据都是为了可缓冲读取来作筹备的,看到其有构造方法会传入一个 InputStream 的实例。理论编码如下
// 被装璜的对象,文件输出流 | |
InputStream in=new FileInputStream("/data/log/app.log"); | |
// 装璜对象,可缓冲 | |
InputStream bufferedIn=new BufferedInputStream(in); | |
bufferedIn.read(); |
这里感觉很眼生吧,其实曾经使用了装璜模式了。
4.2、mybatis 源码中的使用
在 mybatis 中,有个接口 Executor
,顾名思义这个接口是个执行器,它底下有许多实现类,如CachingExecutor
、SimpleExecutor
、BaseExecutor
等等。类图如下:
次要看下 CachingExecutor
类,看着很眼生,很规范的装璜器。其中该类中的 update 是装璜办法,在调用真正 update 办法之前,会执行刷新本地缓存的办法,对原来的 update 做加强和扩大。
public class CachingExecutor implements Executor { | |
private final Executor delegate; | |
private final TransactionalCacheManager tcm = new TransactionalCacheManager(); | |
public CachingExecutor(Executor delegate) { | |
this.delegate = delegate; | |
delegate.setExecutorWrapper(this); | |
} | |
@Override | |
public int update(MappedStatement ms, Object parameterObject) throws SQLException { | |
// 加强内容 | |
// 批改办法就要清空本地的缓存 | |
flushCacheIfRequired(ms); | |
// 调用原有的办法 | |
return delegate.update(ms, parameterObject); | |
} | |
} |
再来看下 BaseExecutor
类,这里有一个 update 办法,这个是本来的被装璜的 update 办法。而后再看这个本来的 update 办法,它调用的 doUpdate 办法是个形象办法,用 protected 润饰。咦,这不就是模板办法么,对于模板办法模式,这里就不开展赘述了。
public abstract class BaseExecutor implements Executor { | |
protected Executor wrapper; | |
@Override | |
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); | |
if (closed) {throw new ExecutorException("Executor was closed."); | |
} | |
clearLocalCache(); | |
return doUpdate(ms, parameter); | |
} | |
protected abstract int doUpdate(MappedStatement ms, Object parameter) | |
throws SQLException; | |
} |
五、总结
长处
- 通过组合而非继承的形式,动静地扩大一个对象的性能,在运行时能够抉择不同的装璜器从而实现不同的性能。
- 无效的防止了应用继承的形式扩大对象性能而带来的灵活性差、子类无限度扩张的问题。
- 具体组件类与具体装璜类能够独立变动,用户能够依据须要新增具体组件类跟装璜类,在应用时在对其进行组合,原有代码毋庸扭转,合乎 ” 开闭准则 ”。
毛病
- 这种比继承更加灵活机动的个性,也同时意味着更加多的复杂性。
- 装璜模式会导致设计中呈现许多小类 (I/O 类中就是这样),如果适度应用,会使程序变得很简单。
- 装璜模式是针对形象组件(Component)类型编程。然而,如果你要针对具体组件编程时,就应该从新思考你的利用架构,以及装璜者是否适合。
六、参考源码
编程文档:https://gitee.com/cicadasmile/butte-java-note | |
利用仓库:https://gitee.com/cicadasmile/butte-flyer-parent |