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

news2024/12/23 22:20:29

对于任何一个软件系统而言,往现有对象中添加新功能是一种不可避免的实现场景,但这一实现过程对现有系统的影响可大可小。从架构设计上讲,我们也知道存在一个开闭原则(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.coloradmin.cn/o/1688736.html

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

相关文章

中文信息期刊投稿邮箱

《中文信息》杂志是国家新闻出版总署批准的国家级刊物&#xff08;月刊&#xff09;&#xff0c;国内外公开发行&#xff0c;大十六开印刷。本刊主要反映我国中文信息处理的学术水平&#xff0c;重点刊登科技、经济、教育等领域的基础理论、科研与应用技术的学术论文&#xff0…

使用Coding部署项目

coding概述&#xff1a;提供一站式开发协作工具&#xff0c;帮助研发团队快速落地敏捷开发与 DevOps 开发方式&#xff0c;实现研发效能升级 一、创建项目 省略 详细文档&#xff1a;https://g-mnbk6665.coding.net/quickstart 二、SSH连接 关于ssh相关命令 重启SSH服务 s…

2023蓝桥杯大赛软件类省赛Java大学B组G题 买二增一 队列的简单应用

用队列 Queue package Dduo; //Bhu Bigdata 1421 //Eslipse IDE 2020-08 //JDK 1.8 //2024/5/19 import java.util.Scanner; import java.math.BigInteger; import java.util.Arrays; import java.util.LinkedList; import java.util.Queue;public class Main {public static v…

【openlayers系统学习】1.6下载要素,将要素数据序列化为 GeoJSON并下载

六、下载要素 下载要素 上传数据并编辑后&#xff0c;我们想让用户下载结果。为此&#xff0c;我们将要素数据序列化为 GeoJSON&#xff0c;并创建一个带有 download​ 属性的 <a>​ 元素&#xff0c;该属性会触发浏览器的文件保存对话框。同时&#xff0c;我们将在地图…

二叉树顺序结构及链式结构

一.二叉树的顺序结构 1.定义&#xff1a;使用数组存储数据&#xff0c;一般使用数组只适合表示完全二叉树&#xff0c;此时不会有空间的浪费 注&#xff1a;二叉树的顺序存储在逻辑上是一颗二叉树&#xff0c;但是在物理上是一个数组&#xff0c;此时需要程序员自己想清楚调整…

GPT-4o: 未来的智能助手

GPT-4o: 未来的智能助手 在这个信息爆炸的时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为我们生活中不可或缺的一部分。作为OpenAI最新推出的语言模型&#xff0c;GPT-4o不仅继承了前几代模型的优点&#xff0c;还在多个方面进行了显著的提升。本文将带你深入了解…

C++:vector基础讲解

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;vector基础讲解》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff01;&#…

网络编程day7

思维导图 数据库编程实现学生管理系统 #include <header.h> #define ID 1 #define NAME 2 #define AGE 3 #define SCORE 4 int do_add(sqlite3 *ppdb) {int add_numb;char add_name[20];int add_age;double add_score;printf("enter student id:");scanf(&quo…

1076: 判断给定有向图是否存在回路

解法&#xff1a; 直观的方法用邻接矩阵dfs,这是错误的代码 #include<iostream> #include<vector> using namespace std; int arr[100][100]; int f 0; void dfs(vector<int>& a, int u) {a[u] 1;for (int i 0; i < a.size(); i) {if (arr[u][i]…

绝缘监测系统在1kV 及以下低压配电系统的应用

安科瑞电气股份有限公司 祁洁 acrelqj 一、系统概述 Acrel-2000L/A 绝缘监测系统设备适用于 1kV 及以下低压配电系统。该设备可以集中采集监测显示绝缘监测仪的数据&#xff0c;实现最多 8 个绝缘监测仪的数据&#xff0c;并且实时记录告警信息和曲线查询。匹配的绝缘监测仪…

bootstrap入门

官方网站&#xff1a;全局 CSS 样式 Bootstrap v3 中文文档 | Bootstrap 中文网 里面各种可以直接用的组件 不全的话可以网上搜索Boostrap常用的按钮样式_btn-large-dim-CSDN博客 怎么在vue项目中使用呢 npm install bootstrap 下载下来然后在main.js加上红框三句后&#…

SpringCloudAlibaba:6.2RocketMQ的普通消息的使用

简介 普通消息也叫并发消息&#xff0c;是发送效率最高&#xff0c;使用最多的一种 依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSch…

1701java药品进销存管理系统Myeclipse开发sqlserver数据库web结构java编程计算机网页项目

一、源码特点 java web药品进销存管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境 为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为s…

解决vue3 vite打包报Root file specified for compilation问题

解决方法&#xff1a; 修改package.json打包命令 把 "build": "vue-tsc --noEmit && vite build" 修改为 "build": "vite build" 就可以了 另外关于allowJs这个问题&#xff0c;在tsconfig.json文件中配置"allowJs&qu…

重学java 39.多线程 — 线程安全

逐渐成为一个情绪稳定且安静成长的人 ——24.5.24 线程安全 什么时候发生&#xff1f; 当多个线程访问同一个资源时&#xff0c;导致了数据有问题&#xff0c;出现并发问题&#xff0c;数据不能及时更新&#xff0c;导致数据发生错误&#xff0c;出现线程安全问题 多线程安全问…

【不太正常的题】LeetCode.232:用栈的函数接口实现队列

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;初阶数据结构刷题 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 &#x1f697; 1.问题描述&#xff1a; 题目中说了只能使用两个栈实现队列&#xff0c;并且只能使用…

【Crypto】password

文章目录 password解题感悟 password 试试flag{zs19900315} 提交成功 解题感悟 这题有点大病

软考--软件设计师--试题六--工厂方法模式(Factory Method)

工厂方法模式(Factory Method) 1、意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪儿一个类&#xff0c;factory method使一个类的实例化延迟到其子类。 2、结构 3、适用性 a、当一个类不知道它所必须创建的对象的类的时候。 b、当一个类希望由它的子类来指定…

UPPAAL使用方法

UPPAAL使用方法 由于刚开始学习时间自动机及其使用方法&#xff0c;对UPPAAL使用不太熟悉&#xff0c;网上能找到的教程很少&#xff0c;摸索了很久终于成功实现一个小例子&#xff0c;所以记录一下详细教程。 这里用到的例子参考【UPPAAL学习笔记】1&#xff1a;基本使用示例…

CyberScheduler调度引擎

CyberScheduler 架构设计 1. 多租户架构&#xff0c;支持 SaaS 化部署和私有化部署 2. 多源异构数据&#xff08;多种集群、数据库&#xff09;、多计算引擎、多类型任务的统一编排调度 3. 灵活资源管理能力&#xff0c;支持不同类型任务的资源管理和资源隔离&#xff0c;优…