草庐IT

聊一聊装饰者模式

知了一笑 2023-04-16 原文

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

一、概述

装饰者模式(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

有关聊一聊装饰者模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  5. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  6. Ruby:标准递归模式 - 2

    我经常迷上ruby​​的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情

  7. ruby - 在 Ruby 中查找多个正则表达式匹配的模式和位置 - 2

    这应该是一个简单的问题,但我找不到任何相关信息。给定一个Ruby中的正则表达式,对于每个匹配项,我需要检索匹配的模式$1、$2,但我还需要匹配位置。我知道=~运算符为我提供了第一个匹配项的位置,而string.scan(/regex/)为我提供了所有匹配模式。如果可能,我需要在同一步骤中获得两个结果。 最佳答案 MatchDatastring.scan(regex)do$1#Patternatfirstposition$2#Patternatsecondposition$~.offset(1)#Startingandendingpo

  8. ruby - sinatra 框架的 MVC 模式 - 2

    我想开始使用“Sinatra”框架进行编码,但我找不到该框架的“MVC”模式。是“MVC-Sinatra”模式或框架吗? 最佳答案 您可能想查看Padrino这是一个围绕Sinatra构建的框架,可为您的项目提供更“类似Rails”的感觉,但没有那么多隐藏的魔法。这是使用Sinatra可以做什么的一个很好的例子。虽然如果您需要开始使用这很好,但我个人建议您将它用作学习工具,以对您来说最有意义的方式使用Sinatra构建您自己的应用程序。写一些测试/期望,写一些代码,通过测试-重复:)至于ORM,你还应该结帐Sequel其中(imho

  9. ruby-on-rails - Rails 如何创建数据模式种子数据 - 2

    有没有一种方法可以自动生成种子数据文件并创建种子数据,就像您在下面链接中的Laravel中看到的那样?LaravelDatabaseMigrations&Seed我在另一个应用程序上看到在Rails的db文件夹下创建了一些带有时间戳的文件,其中包含种子数据。创建它的好方法是什么? 最佳答案 我建议你使用Fabrication的组合gem和Faker.Fabrication允许您编写一个模式来构建您的对象,而Faker为您提供虚假数据,如姓名、电子邮件、电话号码等。这是制造商的样子:Fabricator(:user)dousernam

  10. ruby-on-rails - Ruby on Rails 应用程序的只读模式 - 2

    我有一个交互式RubyonRails应用程序,我想在特定时间将其置于“只读模式”。这将允许用户读取他们需要的数据,但阻止他们执行写入数据库的操作。执行此操作的一种方法是在数据库中放置一个true/false变量,该变量在进行任何写入之前进行检查。我的问题。有没有更优雅的解决方案来解决这个问题? 最佳答案 如果你真的想阻止任何数据库写入,我能想到的最简单的方法是覆盖readonly?始终返回true的模型方法,无论是在选定模型中还是对于所有ActiveRecord模型。如果模型设置为只读(通常通过调用#readonly!来完成),任何

随机推荐