如何彻底搞懂装饰器(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接口及其各种装饰器类所展示的那样。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【简单介绍下7-Zip,什么是7-Zip?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

C++语言学习(五)—— 类与对象(一)

目录 一、类类型的定义 二、类成员的访问控制 2.1 什么是"类内"和"类外" 2.2 对于访问控制属性的说明 三、类类型的使用 3.1 进行抽象 3.2 声明类 3.3 实现类 3.4 使用类 四、构造函数的引入 五、析构函数的引入 六、重载构造函数的引入 6.1 …

一文读懂“双随机、一公开”:企业监管新风向

在这个信息透明化、法治化日益增强的时代&#xff0c;政府对企业监管的方式也在不断创新和完善。“双随机、一公开”作为一种新型监管模式&#xff0c;正逐渐成为市场监管领域的关键词。对于企业或个体工商户而言&#xff0c;了解并适应这一监管机制&#xff0c;对于维护自身合…

工具分享:VsCode注释神器,koro1FileHeader

他是有官方Wiki的。 https://github.com/OBKoro1/koro1FileHeader/wiki/ 项目在GitHub上开源。以下摘录部分wiki&#xff0c;用作介绍分享在这里插入代码片 如何找到setting.json设置模板 简单的输入命令 打开VSCode命令面板: mac: command p window: ctrl p输入> Ope…

9.5 Go语言入门(条件语句和循环语句)

Go语言入门&#xff08;条件语句和循环语句&#xff09; 目录四、条件语句和循环语句1. 条件语句1.1 if 语句1.2 if-else 语句1.3 if-else if-else 语句1.4 带初始化语句的 if1.5 switch 语句1.6 带条件的 switch1.7 多个条件的 case 2. 循环语句2.1 基本 for 循环2.2 省略初始…

从零开始搭建SpringCloud Alibaba微服务架构

Spring Cloud Alibaba是Spring Cloud的一个拓展项目&#xff0c;它用于构建基于阿里巴巴的微服务应用。它提供了多个阿里巴巴的开源组件&#xff0c;如Nacos、Sentinel、Dubbo等&#xff0c;用于解决微服务架构中的服务注册、配置管理、流量控制等问题。 Spring Cloud Alibaba…

记.netcore部署到银河麒麟linux服务器过程详解

一.服务器配置 操作系统:银河麒麟桌面操作系统V10 CPU:intel i5 内存:16G 内核:5.10.0.8-generic 未激活 二.运行环境安装 .netcore 6.0 runtime时安装下载离线包 Download .NET 6.0 (Linux, macOS, and Windows) -下载完后进行解压 sudo su #提权 mkdir -p $HOME/…

TikTok“66大促” 你准备好了吗?

TikTok Shop&#xff0c;也称TikTok电商&#xff0c;作为全球最具创新力和影响力的短视频平台之一&#xff0c;一直以来以其独特的娱乐性内容吸引着数以亿计的用户。而在TikTok Shop上购物&#xff0c;现在已经成为众多年轻用户的时尚选择。随着TikTok Shop开放中国商家入驻&am…

Win10【无线显示器】安装失败(无法添加)解决中。。。

目录 ■失败现象 ■解决方法 【服务启动1】 【服务确认】 【服务启动2】 参考 ■查看电脑是否支持【Miracast无线投屏功能】 参考2 ■解决方法&#xff08;对我的这台电脑有效&#xff09; ■解决后的效果 ■连接时&#xff0c;出现【无法在此设备上查看受保护内容。…

通胀担忧仍存,美联储降息预期或又推迟

KlipC报道&#xff1a;周三&#xff0c;美联储公布4月30日至5月1日政策会议纪要&#xff0c;会议纪要显示美联储对通胀仍感到担忧&#xff0c;将更长时间维持利率不变&#xff0c;必要时进一步收紧政策。 尽管在前不久公布的4月CPI数据显示通胀有所缓解&#xff0c;但是被认为…

【Linux】Centos7安装JDK

【Linux】Centos7安装JDK 下载 Oracle 官网下载 JDK17 https://www.oracle.com/cn/java/technologies/downloads/#java17 安装 使用rz命令上传 jdk tar 包&#xff0c;上传失败直接用 xftp 上传 在安装图形界面时&#xff0c;有勾选开发工具&#xff0c;会自动安装 JDK 需要先…

微信小程序反编译/解包

微信小程序反编译/解包 环境与工具 操作系统&#xff1a;Windows 11 23H2 微信版本&#xff1a;3.9.10.19 Q&#xff1a;如何找到小程序文件位置&#xff1f; A&#xff1a;在微信的设置找到文件路径&#xff0c;小程序文件位于 \WeChat Files\Applet\。 Q&#xff1a;小程…

4K高刷显示器 - 10大产品对比

4K高刷显示器 - 蚂蚁电竞 N27U 显示屏评测报告 一、产品品牌知名度 蚂蚁电竞&#xff0c;作为一家专注于电竞领域的品牌&#xff0c;近年来在市场上逐渐崭露头角。虽然相较于一些老牌显示器品牌&#xff0c;蚂蚁电竞的知名度还有待提高&#xff0c;但其在电竞领域的表现却不容…

秋招突击——算法打卡——5/24——无重复字符的最长字串

题目描述 实现代码 // 无重复字符的最长子串 int lengthOfLongestSubstring(string s) {int l 0,r 0;int res 0;unordered_map<char,int> temp;while(l < s.size()){temp[s.at(l)] l;for (r l 1; r < s.size() ; r) {if(temp.count(s.at(r))) break;else te…

漫画|基于SprinBoot+vue的漫画网站(源码+数据库+文档)

漫画网站 目录 基于SprinBootvue的漫画网站 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大…

【Linux学习】进程间通信 (1) —— 管道

下面是有关进程通信中管道的相关介绍&#xff0c;希望对你有所帮助&#xff01; 小海编程心语录-CSDN博客 1. 进程通信的基本概念 1.1 概念 进程间通信简称 IPC &#xff0c;指两个进程之间的通信。 IPC的方式通常有管道&#xff08;包括无名管道和命名管道&#xff09;、消息…

【Linux网络】端口及UDP

文章目录 1.再看四层2.端口号2.1引入linux端口号和进程pid的区别端口号是如何生成的传输层有了pid还设置端口号端口号划分 2.2问题2.3netstat 3.UDP协议3.0每学一个协议 都要讨论一下问题3.1UDP协议3.2谈udp/tcp实际上是在讨论什么&#xff1f; 1.再看四层 2.端口号 端口号(Po…

//TODO 注释的作用

// TODO 用来标记某处&#xff0c;表示该处含有待办事项&#xff08;尚未解决&#xff09;。其设计本意只是提醒开发者注意&#xff0c;除了代码高亮之外&#xff0c;还可以借助编辑器实现快速定位。 如何使用&#xff1f; 直接在双斜杠后面加 TODO 或者 todo 即可&#xff0c…

CVPR2022医疗图像-GBCNet网络:胆囊癌(GBC)超声(USG)图像检测模型

Surpassing the Human Accuracy:Detecting Gallbladder Cancer from USG Images with Curriculum Learning&#xff1a;超越人类的准确性:基于课程学习的USG图像检测胆囊癌 目录 一、背景与意义 二、介绍 三、网络框架 3.1 区域选择网络 3.2 MS-SoP分类器 3.3 多尺度块 …

抖音运营_抖音电商介绍

截止20年8月&#xff0c;抖音的日活跃数高达6亿。 20年6月&#xff0c;上线抖店 &#xff08;抖音官方电商&#xff09; 一 抖店的定位和特色 1 一站式经营 帮助商家进行 商品交易、店铺管理、客户服务 等全链路的生意经营 2 多渠道拓展 抖音、今日头条、西瓜、抖音火山版…