如何彻底搞懂装饰器(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.rhkb.cn/news/330625.html

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

相关文章

抖音极速版:抖音轻量精简版本,新人享大福利

和快手一样&#xff0c;抖音也有自己的极速版&#xff0c;可视作抖音的轻量精简版&#xff0c;更专注于刷视频看广告赚钱&#xff0c;收益比抖音要高&#xff0c;可玩性更佳。 抖音极速版简介 抖音极速版是一个提供短视频创业和收益任务的平台&#xff0c;用户可以通过观看广…

Spring系列-03-BeanFactory和Application接口和相关实现

BeanFactory BeanFactory和它的子接口们 BeanFactory 接口的所有子接口, 如下图 BeanFactory(根容器)-掌握 BeanFactory是根容器 The root interface for accessing a Spring bean container. This is the basic client view of a bean container; further interfaces such …

【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…

sw套合样条曲线

套合样条曲线,可以变成一条曲线,然后可以进行分段

吉林大学软件工程易错题

1.【单选题】软件工程方法是&#xff08; &#xff09;。 A、为开发软件提供技术上的解决方法 &#xff08;软件工程方法 &#xff09; B、为支持软件开发、维护、管理而研制的计算机程序系统&#xff08;软件工程工具&#xff09; …

码蹄集部分题目(2024OJ赛16期;单调栈集训+差分集训)

&#x1f9c0;&#x1f9c0;&#x1f9c0;单调栈集训 &#x1f96a;单调栈 单调递增栈伪代码&#xff1a; stack<int> st; for(遍历数组) {while(栈不为空&&栈顶元素大于当前元素)//单调递减栈就是把后方判断条件变为小于等于即可{栈顶元素出栈;//同时进行其他…

react antd中transfer穿梭框组件中清除搜索框内容

如图&#xff1a;需要清除search搜索框内容 antd的transfer穿梭框组件未提供入口修改input框的值。 2种方法修改。 1、直接操作dom元素设置值&#xff08;不推荐&#xff09; useEffect(() > {const searchInput document.querySelector(.ant-transfer-list-search input)…

Proteus仿真小技巧(隔空连线)

用了好几天Proteus了.总结一下使用的小技巧. 目录 一.隔空连线 1.打开添加网络标号 2.输入网络标号 二.常用元件 三.运行仿真 四.总结 一.隔空连线 引出一条线,并在末尾点一下. 1.打开添加网络标号 选择添加网络标号, 也可以先点击按钮,再去选择线(注意不要点端口) 2.…

前端绘制流程节点数据

根据数据结构和节点的层级、子节点id&#xff0c;前端自己绘制节点位置和关联关系、指向、已完成节点等 <template><div><div>通过后端节点和层级&#xff0c;绘制出节点以及关联关系等</div><div class"container" ref"container&…

【Numpy】深入解析numpy.mgrid()函数

numpy.mgrid()&#xff1a;多维网格生成与数值计算的利器 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#x1f393…

张大哥笔记:改变自己,才是改变一切的开始

人往往有一种惰性&#xff0c;总喜欢把希望寄托于别人&#xff01;比如会将注意力投向外部因素如环境、他人或命运从而期望为我们的生活带来突破和转机。但现实往往是残酷的&#xff0c;不会发生任何改变的&#xff01;真正的改变来自于自己&#xff0c;自我革新才是改变整个局…

开源实用!猫抓媒体嗅探浏览器插件

CatCatch&#xff1a;网络资源&#xff0c;一触即发 - 精选真开源&#xff0c;释放新价值。 概览 CatCatch是一个专为浏览器设计的资源嗅探扩展&#xff0c;旨在帮助用户轻松捕获和分析网页中的各种资源。无论是视频、音频还是其他类型的文件&#xff0c;猫爪都能提供直观的界…

AI - 各类AI针对Excel分析对比

一个水果销量表&#xff0c;Excel包含多个年份sheet&#xff0c;需要提取某个品种的水果每年的销量&#xff0c;看看几个AI的分析结果吧 1、文心一言3.5&#xff08;不支持Excel&#xff09; 不支持上传Excel文件 2、 通义千问2.5&#xff08;完成★&#xff09; 顺利完成…

虚拟机网络设置为桥接模式后未显示网络

本方法为&#xff0c;VMware配置正确&#xff0c;但在尝试其他办法后未能成功解决的人提供一种方法 本机的虚拟机使用NAT模式正常使用 但是使用桥接模式后重启&#xff0c;未发现虚拟机内网络设置,详见下图&#xff1a; 使用 ifconfig 查看网络详情 发现没有ens33接口 查看硬…

kubectl

陈述式资源管理方法 kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用apiserver的接口 kubectl 是官方的CLI命令行工具&#xff0c;用于与apiserver进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织转换成apiserver能识别的信息&#xff0c;进而实现…

当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!

一、写在开头 依稀记得多年以前的一场面试中&#xff0c;面试官从Java并发编程问到了锁&#xff0c;从锁问到了原子性&#xff0c;从原子性问到了Atomic类库&#xff08;对着JUC包进行了刨根问底&#xff09;&#xff0c;从Atomic问到了CAS算法&#xff0c;紧接着又有追问到了…

B站滑块登录之极验点选

滑块登录这些东西都不是很难&#xff0c;我个人的去处理的话一般会考虑三种方案&#xff0c;一个是自动化selenium 二是各类打码平台 三是ocr识别&#xff0c;本文是selenium接打码平台&#xff0c;也是个比较常规的操作。 先常规步骤跟着来吧&#xff0c;做登录的话把基本的模…

汇聚荣:新手做拼多多应该注意哪些事项?

新手在拼多多开店&#xff0c;面临的是竞争激烈的市场和复杂的运营规则。要想在这个平台上脱颖而出&#xff0c;必须注意以下几个关键事项。 一、市场调研与定位 深入了解市场需求和竞争对手情况是新手开店的首要步骤。选择有潜力的细分市场&#xff0c;并针对目标消费者群体进…

【C语言】指针(三)

目录 一、字符指针 1.1 ❥ 使用场景 1.2 ❥ 有关字符串笔试题 二、数组指针 2.1 ❥ 数组指针变量 2.2 ❥ 数组指针类型 2.3 ❥ 数组指针的初始化 三、数组指针的使用 3.1 ❥ 二维数组和数组名的理解 3.2 ❥ 二维数组传参 四、函数指针 4.1 ❥ 函数的地址 4.2 ❥ 函数…

知识分享:大数据信用花导致的评分不足多久能恢复

随着金融风控领域越来越科技化&#xff0c;基于大数据技术的金融风控成为了贷前风控不可或缺的重要环节&#xff0c;相信很多人在申贷的时候都听说过大数据信用和综合评分等词语&#xff0c;那大数据信用花导致的评分不足多久能恢复呢?本文带大家一起去了解一下。 首先&#x…