如何彻底搞懂装饰器(Decorator)设计模式?

对于任何一个软件系统而言,往现有对象中添加新功能是一种不可避免的实现场景,但这一实现过程对现有系统的影响可大可小。从架构设计上讲,我们也知道存在一个开闭原则(Open-Closed Principle,OCP),也就是说设计需要确保对扩展开放、对修改关闭。


通过开闭原则就能确保新的功能对现有系统的影响最小。那么,问题就来了,开闭原则只是提供了一种方法论支持,我们应该如何来具体实现这一原则呢?方法有很多,而今天我们要介绍的装饰器设计模式就是其中一种具有代表性的实现方式,在Mybatis、Apache ShardingSphere等主流开源框架中应用广泛。

装饰器模式的基本概念和简单示例

在面向对象的世界中,我们通常使用接口来定义业务操作。例如,在如下所示的Shape接口中,我们定义了一个用来绘制形状的操作方法draw。

public interface Shape {

//绘制形状

void draw();

}

有了Shape接口之后,我们来设计两个实现类,分别是Circle和Rectangle。代码X。

public class Circle implements Shape {

@Override

public void draw() {

System.out.println("Shape: Circle");

}

}

public class Rectangle implements Shape {

@Override

public void draw() {

System.out.println("Shape: Rectangle");

}

}

这几个接口和类之间的关系比较简单,如下图所示。


现在,新需求来了,我们需要在绘制形状的基础上对该形状添加边框。显然,这时候就需要对现有的Circle和Rectangle类添加新的功能。基于装饰器模式,我们不是直接对这两个类做出代码上的调整,而是引入一个抽象类ShapeDecorator。

public abstract class ShapeDecorator implements Shape {

protected Shape decoratedShape;

public ShapeDecorator(Shape decoratedShape) {

this.decoratedShape = decoratedShape;

}

public void draw() {

decoratedShape.draw();

}

}

这个ShapeDecorator就是装饰器类,在实现了Shape接口的同时又在内部包含了对Shape的引用,通过这个引用完成对接口方法的实现。这种设计就是装饰器模式的基本实现策略。

然后我们来看ShapeDecorator的一个实现类RedShapeDecorator,该类添加了绘制边框的额外功能,即提供了装饰实现。

public class RedShapeDecorator extends ShapeDecorator {

public RedShapeDecorator(Shape decoratedShape) {

super(decoratedShape);

}

@Override

public void draw() {

decoratedShape.draw();

//添加绘制边框的额外功能

setRedBorder(decoratedShape);

}

private void setRedBorder(Shape decoratedShape) {

System.out.println("Border Color: Red");

}

}

而在具体使用上,我们发现这个装饰类和其他类实际上没有什么区别,即只要是使用Shape接口的地方都可以使用这个包装类。

Shape circle = new Circle();

Shape redCircle = new RedShapeDecorator(new Circle());

Shape redRectangle = new RedShapeDecorator(new Rectangle());

    

circle.draw();

redCircle.draw();

redRectangle.draw();

运行上述代码,我们可以得到如下所示的结果。

Shape: Circle

Shape: Circle

Border Color: Red

Shape: Rectangle

Border Color: Red

上述实现过程虽然比较简单,但已经把一个装饰器模式的完整结构都介绍清楚了。作为总结,我们可以梳理如下所示的类层结构图。


接下来,我们来对装饰器模式的特性做一个总结。从分类上讲,装饰器模式是一种典型的结构型设计模式,允许向一个现有的对象添加新的功能,但又能做到不改变其结构。这种模式创建了一个装饰类,用来对原有类进行包装,并在保持类方法签名完整性的前提下,提供了额外的功能。本质上,装饰器模式的目的是为了动态地给一个对象添加一些额外的职责,相比直接生成子类,这种方式实现起来可以更为灵活。

从使用时机上讲,装饰器模式可以在不想增加很多子类的情况下扩展类,所以通常被认为是继承机制的一个替代模式。正如前面所述的示例一样,具体做法就是将业务功能按职责进行划分并集成装饰者模式。这样装饰类和被装饰类可以独立发展,不会相互耦合。

装饰者模式在Mybatis中的应用与实现

介绍完装饰器模式的基本概念和示例,接下来讨论它的具体应用方式,我们以主流的ORM框架Mybatis为例展开讨论。装饰器模式在Mybatis中的主要应用是在对缓存(Cache)的处理上。在Mybatis中,缓存的功能由根接口Cache定义。

public interface Cache {  

  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();

  default ReadWriteLock getReadWriteLock() {

    return null;

  }

}

围绕Cache接口的类层结构如下图所示。在该图中,Cache接口代表一种抽象,而处于图中央的PerpetualCache代表该接口的具体实现类,位于org.apache.ibatis.cache.impl包中。而其他所有以Cache结尾的类都是装饰器类,位于org.apache.ibatis.cache.decorators包中。


在上图中,整个缓存体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache类实现,该类实际上采用的就是一种基于HashMap的简单实现策略。

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<>();

  public String getId() {…}

  public int getSize() {…}

  public void putObject(Object key, Object value) {…}

  public Object getObject(Object key) {…}

  public Object removeObject(Object key) {…}

  public void clear() {…}

}

可以看到,整个PerpetualCache类的代码结构非常明确,除了一个id属性之外,代表缓存的cache属性只是一个HashMap,是一种典型的基于内存的缓存实现方案。这里的几个方法也比较简单,所有对缓存的操作实际上就是对HashMap的操作。

Mybatis通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方面的控制。用于装饰PerpetualCache的标准装饰器包括BlockingCache、FifoCache、LoggingCache、LruCache等,我们通过名称就可以判断出这些装饰类所要装饰的功能。下图展示了这些缓存类之间的类层关系。


我们无意对所有这些装饰类做全面展开,而是只挑选其中一个来说明装饰器模式的应用方式,这里我们就选择FifoCache,该缓存类提供了FIFO(First Input First Output,先进先出)的缓存数据管理策略。

public class FifoCache implements Cache {

  private final Cache delegate;

  private final Deque<Object> keyList;

  private int size;

  public FifoCache(Cache delegate) {

    this.delegate = delegate;

    this.keyList = new LinkedList<>();

    this.size = 1024;

  }

  @Override

  public String getId() {

    return delegate.getId();

  }

  @Override

  public int getSize() {

    return delegate.getSize();

  }

  public void setSize(int size) {

    this.size = size;

  }

  @Override

  public void putObject(Object key, Object value) {

    cycleKeyList(key);

    delegate.putObject(key, value);

  }

  @Override

  public Object getObject(Object key) {

    return delegate.getObject(key);

  }

  @Override

  public Object removeObject(Object key) {

    return delegate.removeObject(key);

  }

  @Override

  public void clear() {

    delegate.clear();

    keyList.clear();

  }

  private void cycleKeyList(Object key) {

    keyList.addLast(key);

    if (keyList.size() > size) {

      Object oldestKey = keyList.removeFirst();

      delegate.removeObject(oldestKey);

    }

  }

}

以上代码虽然比较冗长,但却简单明了。关键点在于我们引用了Cache接口,并在具体对缓存的各个操作中调用了该接口中的缓存管理方法。因为这里实现的是一个先进先出的策略,所有,我们通过使用一个Deque对象来达到这种效果,这也让我们间接掌握了实现FIFO机制的一种实现方案。

当我们想使用各种缓存类时,可以通过如下所示的方式实现装饰。

Cache cache = new XXXCache(new PerpetualCache("cacheid"))

如果把这里的XXXCache替换成FifoCache就代表着这个新创建的Cache对象具备了FIFO功能。其他缓存装饰器类的使用方法也是一样。

如果你正在考虑往系统对象中添加新功能,不妨先停下来分析所需新功能对现有对象的影响。如果我们需要对现有对象的结构进行比较大的调整,那么说明在类的设计上可能存在不符合开闭原则的坏味道。这时候,我们可以引入今天内容所介绍的装饰器模式对其进行重构。装饰器模式是一种非常有用的设计模式,我们通过基本的实现代码示例给出了它的实现方法。

实现装饰器模式的前提是我们需要采用面向接口的编程模式,然后对功能的类型和职责进行合理的划分,确保不同的装饰器类能够独立承接不同的业务功能。一旦构建了符合装饰器模式的代码框架结构,那么通过构建各种装饰器类,我们就可以为系统添加丰富的新功能。正如Mybatis中Cache接口及其各种装饰器类所展示的那样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/643716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

asp.net core接入prometheus2-自定义指标

前提 了解一下asp.net core接入prometheus快速入门 https://blog.csdn.net/qq_36437991/article/details/139064138 新建.net 8空web项目 安装下面三个包 <PackageReference Include"OpenTelemetry.Exporter.Prometheus.AspNetCore" Version"1.8.0-rc.1&…

真拿AI赚到钱的人,不在朋友圈里

1 最近有张两大AI巨头对比的梗图给我看乐了&#xff0c;玩儿AI的还在做产品&#xff0c;玩儿焦虑的已经在数钱了。 这也是在做AI&#xff0c;只不过是唉声叹气的ai。 要我说&#xff0c;现在缺的根本不是AI&#xff0c;而是【有用的AI】。 恩格斯老师说过一句话&#xff1a…

Java 对接百度网盘

文章目录 前言一、创建百度网盘账号二、代码实现1. 常量类2. 工具类3. 授权码模式授权4. 文件分片上传&#xff08;可获取进度&#xff09;--方法一5. 文件下载(可获取进度)--方法一6. 获取文件列表7. 文件分片上传&#xff08;不可获取进度&#xff09;--方法二7. 文件下载&am…

算法之堆排序

堆排序是一种基于比较的排序算法&#xff0c;通过构建二叉堆&#xff08;Binary Heap&#xff09;&#xff0c;可以利用堆的性质进行高效的排序。二叉堆是一个完全二叉树&#xff0c;可以有最大堆和最小堆两种形式。在最大堆中&#xff0c;父节点的值总是大于或等于其子节点的值…

C++与Android处理16进制大端/小端数据实例(二百七十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

“大数据建模、分析、挖掘技术应用研修班”的通知!

随着2015年9月国务院发布了《关于印发促进大数据发展行动纲要的通知》&#xff0c;各类型数据呈现出了指数级增长&#xff0c;数据成了每个组织的命脉。今天所产生的数据比过去几年所产生的数据大好几个数量级&#xff0c;企业有了能够轻松访问和分析数据以提高性能的新机会&am…

夏日采摘季,视频智能监控管理方案助力智慧果园管理新体验

5月正值我国各地西瓜、杨梅、大樱桃、油桃等水果丰收的季节&#xff0c;许多地方都举办了采摘旅游活动&#xff0c;吸引了众多游客前来体验采摘乐趣。随着采摘的人流量增多&#xff0c;果园的管理工作也面临压力。 为了提升水果园采摘活动的管理效果&#xff0c;减少人工巡查成…

harbor 认证

Harbor 认证过程 Harbor以 Docker Registry v2认证为基础&#xff0c;添加上一层权限保护。1.v2 集成了一个安全认证的功能&#xff0c;将安全认证暴露给外部服务&#xff0c;让外部服务去实现2.强制用户每次Docker pull/push请求都要带一个合法的Token&#xff0c;Registry会…

基于jeecgboot-vue3的Flowable新建流程定义(一)

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、vue3版本因为流程分类是动态的&#xff0c;不再固定了&#xff0c;所以新建的时候需要选择建立哪种流程类型的流程 代码如下&#xff1a; <!-- 选择模型的流程类型对话框 -->&…

JDBCTemplate介绍

Spring JDBC Spring框架对Spring的简单封装。提供一个JDBCTemplate对象简化JDBC开发 *步骤&#xff1a; 1、导入jar包 2、创建JDBCTemplate对象。依赖于数据源DataSource *JdbcTemplate templatenew JdbcTemplate(ds); 3、调用JdbcTemplate的方法来完成CRUD的操作 *update()&…

【实战教程】使用Spring AOP和自定义注解监控接口调用

一、背景 随着项目的长期运行和迭代&#xff0c;积累的功能日益繁多&#xff0c;但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。 为了提高系统性能和代码质量&#xff0c;我们往往需要对那些不常用的功能进行下线处理。 那么&#xff0c;该下线哪些功能呢&…

代码随想录-Day18

513. 找树左下角的值 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 方法一&#xff1a;深度优先搜索 class Solution {int curVal 0;int curHeight 0;public int findBottomLeftValue(TreeNode roo…

GitLens或者Git Graph在vscode中对比文件历史变化,并将历史变化同步到当前文件中

有时候我们上周改的代码&#xff0c;现在想反悔把它恢复过来&#xff0c;怎么办&#xff1f;&#xff1f;&#xff1f;很好&#xff0c;你有这个需求&#xff0c;说明你找对人了&#xff0c;那就是我们需要在vscode中安装这个插件&#xff1a;GitLens或者Git Graph&#xff0c;…

做抖店四年来的经验分享,想做抖店的多看看,给你揭露真正的抖店

大家好&#xff0c;我是电商花花。 我做抖音小店从21年就已经开始了&#xff0c;中间一直都没断过&#xff0c;一直都抖店无货源&#xff0c;从刚开始的一家店铺&#xff0c;到现在的80多家店铺&#xff0c;不断完善和总结我们做店的方法。 在我看来做抖音小店现在很简单&…

Linux服务升级:Twemproxy 升级 Redis代理

目录 一、实验 1.环境 2.多实例Redis部署 3.Twemproxy 升级Redis代理 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 系统版本软件IP备注CentOS7.9Twemproxy192.168.204.200 Redis代理 Redis127.0.0.1:6379第一个Redis实例 Redis127.0.0.1:6380第二个…

别被“涨价“带跑,性价比才是消费真理

文章来源&#xff1a;全食在线 “再不好好赚钱&#xff0c;连方便面也吃不起了。”这是昨天在热搜下&#xff0c;一位网友的留言。而热搜的内容&#xff0c;正是康师傅方便面即将涨价的消息。 01 传闻初现 昨天上午&#xff0c;朋友圈就有人放出康师傅方便面要涨价的消息&am…

Py之llama-parse:llama-parse(高效解析和表示文件)的简介、安装和使用方法、案例应用之详细攻略

Py之llama-parse&#xff1a;llama-parse(高效解析和表示文件)的简介、安装和使用方法、案例应用之详细攻略 目录 llama-parse的简介 llama-parse的安装和使用方法 1、安装 2、使用方法 第一步&#xff0c;获取API 密钥 第二步&#xff0c;安装LlamaIndex、LlamaParse L…

从ZooKeeper切换到ClickHouse-Keeper,藏着怎样的秘密

本文字数&#xff1a;7772&#xff1b;估计阅读时间&#xff1a;20 分钟 作者&#xff1a;博睿数据 李骅宸&#xff08;太道&#xff09;& 小叮当 本文在公众号【ClickHouseInc】首发 本系列前两篇内容&#xff1a; 从ES到ClickHouse&#xff0c;Bonree ONE平台更轻更快&a…

API攻击呈指数级增长,如何保障API安全?

从远程医疗、共享汽车到在线银行&#xff0c;实时API是构建数字业务的基础。然而&#xff0c;目前超过90%的基于Web的网络攻击都以API端点为目标&#xff0c;试图利用更新且较少为人所知的漏洞&#xff0c;而这些漏洞通常是由安全团队未主动监控的API所暴露&#xff0c;致使API…

【设计模式】JAVA Design Patterns——Callback(回调模式)

&#x1f50d;目的 回调是一部分被当为参数来传递给其他代码的可执行代码&#xff0c;接收方的代码可以在一些方便的时候来调用它。 &#x1f50d;解释 真实世界例子 我们需要被通知当执行的任务结束时。我们为调用者传递一个回调方法然后等它调用通知我们。 通俗描述 回调是一…