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

5次阅读

共计 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