聊一聊装饰者模式

打印 上一主题 下一主题

主题 917|帖子 917|积分 2751

是你,还是你,一切都有你!—— 装饰者模式
一、概述

装饰者模式(Decorator Pattern)允许向一个现有的对象扩展新的功能,同时不改变其结构。主要解决直接继承下因功能的不断横向扩展导致子类膨胀的问题,无需考虑子类的维护。
装饰者模式有4种角色:

  • 抽象构件角色(Component):具体构件类和抽象装饰者类的共同父类。
  • 具体构件角色(ConcreteComponent):抽象构件的子类,装饰者类可以给它增加额外的职责。
  • 装饰角色(Decorator):抽象构件的子类,具体装饰类的父类,用于给具体构件增加职责,但在子类中实现。
  • 具体装饰角色(ConcreteDecorator):具体装饰类,定义了一些新的行为,向构件类添加新的特性。

二、入门案例

2.1、类图


2.2、基础类介绍
  1. // 抽象构件角色
  2. public interface Component {
  3.     void doSomeThing();
  4. }
  5. // 具体构件角色
  6. public class ConcreteComponent implements Component {
  7.     @Override
  8.     public void doSomeThing() {
  9.         System.out.println("处理业务逻辑");
  10.     }
  11. }
  12. // 装饰者类
  13. public abstract class Decorator implements Component {
  14.     private Component component;
  15.     public Decorator(Component component) {
  16.         this.component = component;
  17.     }
  18.     @Override
  19.     public void doSomeThing() {
  20.         // 调用处理业务逻辑
  21.         component.doSomeThing();
  22.     }
  23. }
  24. // 具体装饰类
  25. public class ConcreteDecorator extends Decorator {
  26.     public ConcreteDecorator(Component component) {
  27.         super(component);
  28.     }
  29.     @Override
  30.     public void doSomeThing() {
  31.         System.out.println("业务逻辑功能扩展");
  32.         super.doSomeThing();
  33.     }
  34. }
复制代码
当然,如果需要扩展更多功能的话,可以再定义其他的ConcreteDecorator类,实现其他的扩展功能。
如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
三、应用场景

如风之前在一家保险公司干过一段时间。其中保险业务员也会在自家产品注册账号,进行推销。不过在这之前,他们需要经过培训,导入一张展业资格证书。然后再去推销保险产品供用户下单,自己则通过推销产生的业绩,参与分润,拿对应的佣金。
对于上面导证书这个场景,实际上是会根据不同的保险产品,导入不同的证书的。并且证书的类型也不同,对应的解析、校验、执行的业务场景都是不同的。如何去实现呢?当然if-else确实也是一种不错的选择。下面放一段伪代码
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022/11/17 11:32
  5. * @description
  6. */
  7. @RestController
  8. @RequestMapping("/certificate")
  9. public class CertificateController {
  10.     @Resource
  11.     private CommonCertificateService certificateService;
  12.     @PostMapping("/import")
  13.     public Result<Integer> importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {
  14.         return Result.success(certificateService.importCertificate(file, productCode));
  15.     }
  16. }
  17. /**
  18. * @author 往事如风
  19. * @version 1.0
  20. * @date 2022/11/17 13:25
  21. * @description
  22. */
  23. @Service
  24. public class CommonCertificateService {
  25.     public Integer importCertificate(MultipartFile file, String productCode) {
  26.         // 1、参数非空校验
  27.         // 2、通过file后缀判断file类型,支持excel和pdf
  28.         // 3、解析file文件,获取数据,统一封装到定义的CertificatePojo类中
  29.         // 4、根据产品类型判断导入之前的业务逻辑
  30.         if (productCode.equals(DecorateConstants.PRODUCT_A)) {
  31.             // 重新计算业绩逻辑
  32.             // 重新算业绩类型逻辑
  33.             // 一坨坨代码去实现....
  34.         }
  35.         else if (productCode.equals(DecorateConstants.PRODUCT_B)) {
  36.             // 导入证书的代理人自己以及上级身份晋升逻辑
  37.             // 业绩计算逻辑
  38.             // 一坨坨代码去实现...
  39.         } else if (productCode.equals(DecorateConstants.PRODUCT_C)) {
  40.             // c产品下的业务逻辑
  41.             // 一坨坨代码去实现...
  42.         } else {
  43.             // 默认的处理逻辑
  44.             // 一坨坨代码去实现...
  45.         }
  46.         // 5、证书数据保存
  47.         // 6、代理人信息保存
  48.         // 7、相关流水数据保存
  49.         // 返回代理人id
  50.         Integer agentId = Integer.MAX_VALUE;
  51.         return agentId;
  52.     }
  53. }
复制代码
从上面的伪代码看到,所有的业务逻辑是在一起处理的,通过productCode去处理对应产品的相关逻辑。这么一看,好像也没毛病,但是还是被技术大佬给否决了。好吧,如风决定重写。运用装饰者模式,重新处理下了下这段代码。
1、一切再从注解出发,自定义Decorate注解,这里定义2个属性,scene和type

  • scene:标记具体的业务场景
  • type:表示在该种业务场景下,定义一种具体的装饰器类
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022/11/8 17:44
  5. * @description
  6. */
  7. @Target({ElementType.TYPE})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Documented
  10. @Inherited
  11. @Service
  12. public @interface Decorate {
  13.      /**
  14.       * 具体的业务场景
  15.       * @return
  16.       */
  17.      String scene();
  18.      /**
  19.       * 类型:不同业务场景下,不同的装饰器类型
  20.       * @return
  21.       */
  22.      String type();
  23. }
复制代码
2、抽象构件接口,BaseHandler,这个是必须滴
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022/11/8 17:07
  5. * @description 抽象处理接口
  6. */
  7. public interface BaseHandler<T, R> {
  8.     /**
  9.      * 统一的处理方法
  10.      * @param t
  11.      * @return
  12.      */
  13.     R handle(T t);
  14. }
复制代码
3、抽象装饰器类,AbstractHandler,持有一个被装饰类的引用,这个引用具体在运行时被指定
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022-11-13 22:10:05
  5. * @desc 抽象父类
  6. */
  7. public abstract class AbstractHandler<T, R> implements BaseHandler<T, R> {
  8.     protected BaseHandler service;
  9.     public void setService(BaseHandler service) {
  10.         this.service = service;
  11.     }
  12. }
复制代码
4、具体的装饰器类AProductServiceDecorate,主要负责处理“导师证书”这个业务场景下,A产品相关的导入逻辑,并且标记了自定义注解Decorate,表示该类是装饰器类。主要负责对A产品证书导入之前逻辑的增强,我们这里称之为“装饰”。
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022-11-13 23:11:16
  5. * @desc
  6. */
  7. @Decorate(scene = SceneConstants.CERTIFICATE_IMPORT, type = DecorateConstants.PRODUCT_A)
  8. public class AProductServiceDecorate extends AbstractHandler<MultipartFile, Integer> {
  9.     /**
  10.      * 重写父类处理数据方法
  11.      * @param file
  12.      * @return
  13.      */
  14.     @Override
  15.     public Integer handle(MultipartFile file) {
  16.         // 解析
  17.         CertificatePojo data = parseData(file);
  18.         // 校验
  19.         check(data);
  20.         // 业绩计算
  21.         calAchievement(data.getMobile());
  22.         return (Integer) service.handle(data);
  23.     }
  24.     public CertificatePojo parseData(MultipartFile file) {
  25.         // file,证书解析
  26.         System.out.println("A产品的证书解析......");
  27.         CertificatePojo certificatePojo = new CertificatePojo();
  28.         certificatePojo.setMobile("12323");
  29.         certificatePojo.setName("张三");
  30.         certificatePojo.setMemberNo("req_343242ds");
  31.         certificatePojo.setEffectDate("2022-10-31:20:20:10");
  32.         return certificatePojo;
  33.     }
  34.     /**
  35.      * 证书数据校验
  36.      * @param data
  37.      */
  38.     public void check(CertificatePojo data) {
  39.         // 数据规范和重复性校验
  40.         // .....
  41.         System.out.println("A证书数据校验......");
  42.     }
  43.     /**
  44.      * 计算业绩信息
  45.      */
  46.     private void calAchievement(String mobile) {
  47.         System.out.println("查询用户信息, 手机号:" + mobile);
  48.         System.out.println("重新计算业绩...");
  49.     }
  50. }
复制代码
当然,还是其他装饰类,BProductServiceDecorate,CProductServiceDecorate等等,负责装饰其他产品,这里就不举例了。
5、当然还有管理装饰器类的装饰器类管理器DecorateManager,内部维护一个map,负责存放具体的装饰器类
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022/11/15 17:18
  5. * @description 装饰管理器
  6. */
  7. public class DecorateManager {
  8.     /**
  9.      * 用于存放装饰器类
  10.      */
  11.     private Map<String, AbstractHandler> decorateHandleMap = new HashMap<>();
  12.     /**
  13.      * 将具体装饰器类放在map中
  14.      *
  15.      * @param handlerList
  16.      */
  17.     public void setDecorateHandler(List<AbstractHandler> handlerList) {
  18.         for (AbstractHandler h : handlerList) {
  19.             Decorate annotation = AnnotationUtils.findAnnotation(h.getClass(), Decorate.class);
  20.             decorateHandleMap.put(createKey(annotation.scene(), annotation.type()), h);
  21.         }
  22.     }
  23.     /**
  24.      * 返回具体的装饰器类
  25.      *
  26.      * @param type
  27.      * @return
  28.      */
  29.     public AbstractHandler selectHandler(String scene, String type) {
  30.         String key = createKey(scene, type);
  31.         return decorateHandleMap.get(key);
  32.     }
  33.     /**
  34.      * 拼接map的key
  35.      * @param scene
  36.      * @param type
  37.      * @return
  38.      */
  39.     private String createKey(String scene, String type) {
  40.         return StrUtil.builder().append(scene).append(":").append(type).toString();
  41.     }
  42. }
复制代码
6、用了springboot,当然需要将这个管理器交给spring的bean容器去管理,需要创建一个配置类DecorateAutoConfiguration
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022-11-12 19:22:41
  5. * @desc
  6. */
  7. @Configuration
  8. public class DecorateAutoConfiguration {
  9.     @Bean
  10.     public DecorateManager handleDecorate(List<AbstractHandler> handlers) {
  11.         DecorateManager manager = new DecorateManager();
  12.         manager.setDecorateHandler(handlers);
  13.         return manager;
  14.     }
  15. }
复制代码
7、被装饰的service类,CertificateService,只需要关注自己的核心逻辑就可以
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022/11/8 17:10
  5. * @description 执行证书导入的service
  6. */
  7. @Service
  8. public class CertificateService implements BaseHandler<CertificatePojo, Integer> {
  9.     /**
  10.      * 处理导入证书的核心逻辑service
  11.      * @param certificate
  12.      * @return
  13.      */
  14.     @Override
  15.     public Integer handle(CertificatePojo certificate) {
  16.         System.out.println("核心业务,证书数据:" + JSONUtil.toJsonStr(certificate));
  17.         // 1、证书数据保存
  18.         // 2、代理人信息保存
  19.         // 3、相关流水数据保存
  20.         // 其他的一些列核心操作
  21.         Integer agentId = Integer.MAX_VALUE;
  22.         // 返回代理人id
  23.         return agentId;
  24.     }
  25. }
复制代码
8、在原来的controller中,注入管理器类DecorateManager去调用,以及service,也就是被装饰的类。首先拿到装饰器,然后再通过setService方法,传入被装饰的service。也就是具体装饰什么类,需要在运行时才确定。
  1. /**
  2. * @author 往事如风
  3. * @version 1.0
  4. * @date 2022-11-13 23:30:37
  5. * @desc
  6. */
  7. @RestController
  8. public class WebController {
  9.     @Resource
  10.     private DecorateManager decorateManager;
  11.     @Resource
  12.     private CertificateService certificateService;
  13.     @PostMapping("/import")
  14.     public Result importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {
  15.         AbstractHandler handler = decorateManager.selectHandler(SceneConstants.CERTIFICATE_IMPORT, productCode);
  16.         if (Objects.isNull(handler)) {
  17.             return Result.fail();
  18.         }
  19.         handler.setService(certificateService);
  20.         return Result.success(handler.handle(file));
  21.     }
  22. }
复制代码
下面模拟下代理人导入证书的流程,当选择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的一段代码
[code]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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我可以不吃啊

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表