关于后端:聊一聊装饰者模式

64次阅读

共计 10344 个字符,预计需要花费 26 分钟才能阅读完成。

是你,还是你,所有都有你!—— 装璜者模式

一、概述

装璜者模式(Decorator Pattern)容许向一个现有的对象扩大新的性能,同时不扭转其构造。次要解决间接继承下因性能的一直横向扩大导致 子类收缩 的问题,无需思考子类的保护。

装璜者模式有 4 种角色:

  1. 形象构件角色(Component):具体构件类和形象装璜者类的独特父类。
  2. 具体构件角色(ConcreteComponent):形象构件的子类,装璜者类能够给它减少额定的职责。
  3. 装璜角色(Decorator):形象构件的子类,具体装璜类的父类,用于给具体构件减少职责,但在子类中实现。
  4. 具体装璜角色(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("从新计算业绩...");
    }
    }

    当然,还是其余装璜类,BProductServiceDecorateCProductServiceDecorate等等,负责装璜其余产品,这里就不举例了。
    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 流中,InputStreamFilterInputStreamFileInputStreamBufferedInputStream的一段代码

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,顾名思义这个接口是个执行器,它底下有许多实现类,如CachingExecutorSimpleExecutorBaseExecutor 等等。类图如下:

次要看下 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;
}

五、总结

长处

  1. 通过组合而非继承的形式,动静地扩大一个对象的性能,在运行时能够抉择不同的装璜器从而实现不同的性能。
  2. 无效的防止了应用继承的形式扩大对象性能而带来的灵活性差、子类无限度扩张的问题。
  3. 具体组件类与具体装璜类能够独立变动,用户能够依据须要新增具体组件类跟装璜类,在应用时在对其进行组合,原有代码毋庸扭转,合乎 ” 开闭准则 ”。

毛病

  1. 这种比继承更加灵活机动的个性,也同时意味着更加多的复杂性。
  2. 装璜模式会导致设计中呈现许多小类 (I/O 类中就是这样),如果适度应用,会使程序变得很简单。
  3. 装璜模式是针对形象组件(Component)类型编程。然而,如果你要针对具体组件编程时,就应该从新思考你的利用架构,以及装璜者是否适合。

六、参考源码

编程文档:https://gitee.com/cicadasmile/butte-java-note
利用仓库:https://gitee.com/cicadasmile/butte-flyer-parent

正文完
 0