【设计模式】装饰器模式
装饰器模式
以生活中的场景来举例,一个蛋糕胚,给它涂上奶油就变成了奶油蛋糕,再加上巧克力和草莓,它就变成了巧克力草莓蛋糕。
像这样在不改变原有对象的基础之上,将功能附加到原始对象上的设计模式就称为装饰模式(Decorator模式),属于结构型模式。
装饰器模式中主要有四个角色:
Component : 定义被装饰对象的接口,装饰器也需要实现一样的接口
ConcreteComponent: 具体的装饰对象,实现了Component接口,通常就是被装饰的原始对象
Decorator: 所有装饰器的抽象父类,需要定义与Component一致的接口,并且持有一个被装饰的Component对象
ConcreteDecorator: 具体装饰器对象

代码示例
假设我们需要设计一个奖金的系统,目前一个工作人员的奖金包含三部分:
本月销售额的奖金 : 当月销售额的3%
累计销售额的奖金 : 累计销售额的0.1%
团队销售额的奖金 : 团队销售额的1%,只有经理才有
使用装饰模式的实现如下 : 定义一个所有奖金的抽象接口,然后定义一个BasicPrize的初始奖金对象,不同的人奖金的组成不同,就为其添加不同的装饰器
public abstract class Component {abstract double calPrize(String userName);
}
public class BasicPrize extends Component {//保存了一个员工与销售额的对应关系的mappublic static Map<String,Double> saleMoney = new HashMap<String,Double>();static {saleMoney.put("小明",9000.0);saleMoney.put("小陈",20000.0);saleMoney.put("小王",30000.0);saleMoney.put("张经理",55000.0);}public double calPrize(String userName) {return 0;}
}
/**** 所有装饰器的抽象父类,持有一个被装饰对象*/
public abstract class Decorator extends Component {protected Component component;public Decorator(Component component){this.component = component;}public abstract double calPrize(String userName);
}
/*** * 当月奖金计算规则*/
public class MonthPrizeDecorator extends Decorator{public MonthPrizeDecorator(Component component) {super(component);}public double calPrize(String userName) {//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)double money = this.component.calPrize(userName);//计算本奖金 对应员工的业务额的3%double prize = BasicPrize.saleMoney.get(userName)*0.03;System.out.println(userName+"当月 业务奖金:"+prize);return money + prize;}}
/*** 累计奖金*/
public class SumPrizeDecorator extends Decorator {public SumPrizeDecorator(Component component) {super(component);}public double calPrize(String userName) {//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)double money = this.component.calPrize(userName);//计算本奖金 累计业务额的0.1% 假设是100000double prize = 100000*0.001;System.out.println(userName+"当月 累计奖金:"+prize);return money + prize;}}
/*** 团队奖金*/
public class GroupPrizeDecorator extends Decorator {public GroupPrizeDecorator(Component component) {super(component);}public double calPrize(String userName) {//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)double money = this.component.calPrize(userName);//计算本奖金 本团队的业务额的1%double sumSale = 0;for(double sale: BasicPrize.saleMoney.values()){sumSale += sale;}double prize = sumSale*0.01;System.out.println(userName+"当月 团队奖金:"+prize);return money + prize;}}
/*** @Description*/
public class Client {public static void main(String[] args) throws FileNotFoundException {//基础对象 奖金0Component basePrice = new BasicPrize();//当月奖金Component salePrize = new MonthPrizeDecorator(basePrice);//累计奖金Component sumPrize = new SumPrizeDecorator(salePrize);//团队奖金Component groupPrize = new GroupPrizeDecorator(sumPrize);//普通员工的奖金由两部分组成double d1 = sumPrize.calPrize("小明");System.out.println("========================小明总奖金:"+d1);double d2 = sumPrize.calPrize("小陈");System.out.println("========================小陈总奖金:"+d2);double d3 = sumPrize.calPrize("小王");System.out.println("========================小王总奖金:"+d3);//王经理的奖金由三部分组成double d4 = groupPrize.calPrize("张经理");System.out.println("========================张经理总奖金:"+d4);}
}
输出结果如下:

这里我们使用装饰器模式,主要是为了方便组合和复用。在一个继承的体系中,子类往往是互斥的,比方在一个奶茶店,它会有丝袜奶茶,红茶,果茶等,用户想要一杯饮料,一般都会在这些种类中选一种,不能一杯饮料既是果茶又是奶茶。然后用户可以根据自己的喜好添加任何想要的decorators,珍珠,椰果,布丁等,这些添加物对所有茶类饮品都是相互兼容的,并且是可以被允许反复添加的(同样的装饰器是否允许在同一个对象上装饰多次,视情况而定,像上面的奖金场景显然是不被允许的)
jdk中的装饰器模式
java.io包是用于输入输出的包,这里使用了大量的装饰器模式,我们再来体会一下装饰器模式的优点。
下图是jdk 输出流的一部分类图,很明显是一个装饰模式的类图,OutputStream是顶层父类,FileOutputStream和ObjectOutputStream是具体的被装饰类,FilterOutputStream是所有输出流装饰器的抽象父类

使用的代码如下:
InputStream in = new FileInputStream("/user/wangzheng/test.txt");InputStream bin = new BufferedInputStream(in);byte[] data = new byte[128];while (bin.read(data) != -1) {//...}
为什么Java IO设计的时候要使用装饰器模式, 而J不设计⼀个继承 FileInputStream 并且⽀持缓存的 BufferedFileInputStream 类呢?
如果InputStream 只有⼀个⼦类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计⼀个孙⼦类BufferedFileInputStream,也是可以接受的。但实际上,继承 InputStream 的⼦类有很多。我们需要给每⼀个 InputStream 的⼦类,
再继续派⽣⽀持缓存读取的⼦类。
除此之外,我们还需要对功能进⾏其他⽅⾯的增强,⽐如下⾯的DataInputStream 类,⽀持按照基本数据类型(int、boolean、long 等)来读取数据。这种情形下,使用继承的方式的话类的继承结构变得⽆⽐复杂,代码维护起来也比较费劲。
按照装饰器模式的结构,我们可以继承FilterOutputStream实现自定义的装饰器,并且在使用的时候可以和jdk自带的装饰器对象任意组合。
我们可以实现一个简单的复制输出内容的OutputStream装饰器
public class DuplicateOutputStream2 extends FilterOutputStream {/*** Creates an output stream filter built on top of the specified* underlying output stream.** @param out the underlying output stream to be assigned to* the field <tt>this.out</tt> for later use, or* <code>null</code> if this instance is to be* created without an underlying stream.*/public DuplicateOutputStream2(OutputStream out) {super(out);}//将所有的内容复制一份输出 ab 变成aabbpublic void write(int b) throws IOException {super.write(b);super.write(b);}
}
装饰器类是否可以直接实现Component父类?
以输出流为例,如果直接继承OutputStream来实现自定义装饰器
public class DuplicateOutputStream extends OutputStream {private OutputStream os;public DuplicateOutputStream(OutputStream os){this.os = os;}//将所有的内容复制一份输出 ab 变成aabbpublic void write(int b) throws IOException {os.write(b);os.write(b);}
}
public class ClientTest {public static void main(String[] args) throws IOException {DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream2(new FileOutputStream("1.txt"))));testOutputStream(dataOutputStream);DataOutputStream dataOutputStream1 = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream(new FileOutputStream("1.txt"))));testOutputStream(dataOutputStream1);}public static void testOutputStream(DataOutputStream dataOutputStream) throws IOException {DataInputStream dataInputStream = new DataInputStream(new FileInputStream("1.txt"));dataOutputStream.write("bdsaq".getBytes());dataOutputStream.close();System.out.println(dataInputStream.available());byte[] bytes3 = new byte[dataInputStream.available()];dataInputStream.read(bytes3);System.out.println("文件内容:"+new String(bytes3));}
}
输出结果:

乍一看好像没什么区别,但是如果把BufferedOutputStream装饰器和自定义的装饰器互换。
DataOutputStream dataOutputStream2 = new DataOutputStream(new DuplicateOutputStream2(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream2);DataOutputStream dataOutputStream3 = new DataOutputStream(new DuplicateOutputStream(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream3);
输出结果:

使用了实现OutputStream的DuplicateOutputStream会出现没有正常输出数据,这是因为我们使用了BufferedOutputStream这个带缓存区的输出流,缓存区的输出流在缓存区没有满的情形下是不会进行输出操作的。一般情形下我们在调用jdk的DataOutputStream的close方法的时候会调用其传入的输出流的flush()方法,并且向下传递调用,BufferedOutputStream里的数据会正常输出。
在使用DuplicateOutputStream2的时候其调用关系是这样的:
dataOutputStream.close()–>duplicateOutputStream2.flush()–>bufferedOutputStream.flush()
使用DuplicateOutputStream的时候由于DuplicateOutputStream继承的OutputStream的flush()方法是空实现,所以不会继续往下调用bufferedOutputStream的flush()方法,故而最后没有得到输出内容
所以装饰器类需要继承Decorator抽象父类,而不是直接继承Component抽象类,我认为是为了在Decorator里实现一些共性的代码,以便在使用装饰器的时候能够更加自由,无视其组合顺序 (当然如果你的Decorator里没有任何逻辑代码,在合适的场景下你可以不定义抽象装饰器类)
总结
装饰器模式主要用于解决继承关系过于复杂的问题,通过组合来替代继承。
它主要的作⽤是给原始类添加增强功能。
除此之外,装饰器模式还有⼀个特点,那就是可以对原始类嵌套使⽤多个装饰器。为了满⾜这个应⽤场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接⼝。
相关文章:
【设计模式】装饰器模式
装饰器模式 以生活中的场景来举例,一个蛋糕胚,给它涂上奶油就变成了奶油蛋糕,再加上巧克力和草莓,它就变成了巧克力草莓蛋糕。 像这样在不改变原有对象的基础之上,将功能附加到原始对象上的设计模式就称为装饰模式(D…...
Nginx配置实例-反向代理案例一
实现效果:使用nginx反向代理,访问 www.suke.com 直接跳转到本机地址127.0.0.1:8080 一、准备工作 Centos7 安装 Nginxhttps://liush.blog.csdn.net/article/details/125027693 1. 启动一个 tomcat Centos7安装JDK1.8https://liush.blog.csdn.net/arti…...
Java中IO流中字节流(FileInputStream(read、close)、FileOutputStream(write、close、换行写、续写))
IO流:存储和读取数据的解决方案 纯文本文件:Windows自带的记事本打开能读懂 IO流体系: FileInputStream:操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来 书写步骤:①创建字节输入流对象 …...
C#完全掌握控件之-combbox
无论是QT还是VC,这些可视化编程的工具,掌握好控件的用法是第一步,C#的控件也不例外,尤其这些常用的控件。常见控件中较难的往往是这些与数据源打交道的,比如CombBox、ListBox、ListView、TreeView、DataGridView. 文章…...
STL的空间配置器(allocator)
简答: 在CSTL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。 解析: 1.两种C类对象实例化方式的异同在c中&a…...
linux系统莫名其妙的环境变量问题
今天使用Ubuntu20.04系统,使用less命令查看日志,发现日志中的“中文”显示为乱码; 使用vim命令查看该日志文件也显示为乱码; 使用more命令查看该日志文件则显示正常。 首先查询系统的字符集编码,发现编码正常支持中…...
使用 Microsoft Dataverse 简化的连接快速入门
重复昨天本地部署dynamics实例将其所有的包删除之后,再次重新下载回来。运行填写跟之前登陆插件一样的信息点击login 然后查看控制台,出现这样就说明第一个小示例就完成了。查看你的dy365平台下的 “我的活动”就可以看到刚刚通过后台代码创建的东西了。…...
PLSQL Developer 安装指南
PLSQL Developer 是 Oracle 的客户端。 下面以64位破解版的PLSQL Developer为例,进行PLSQL Developer 安装讲解。 0. 下载 PLSQL Developer https://download.csdn.net/download/Shipley_Leo/87557938 1. 根据操作系统选择对应“plsqldev.exe”可执行文件ÿ…...
腾讯云企业网盘2.5版本全新发布啦!!!
腾讯云企业网盘又又又更新啦!本期重点打磨管理协同、企业安全守护能力,同时也不断强化自身产品体验,助力企业高效办公~那么,此次更新具体有什么安全可靠的新功能呢?今天就带大家一起解锁~01协同管理,提升工…...
Excel职业版本(4)
图表 图表基本结构 组成元素 图表的分类 柱状图 介绍:在竖直方向比较不同类型的数据 适用场景:用于二维数据集,对于不同类型的数据进行对比,也可用于同一类型的数据在不同的时间维度的数据对比,通过柱子的高度来反…...
3-2 SpringCloud快速开发入门:Ribbon 实现客户端负载均衡
接上一章节Ribbon 是什么,这里讲讲Ribbon 实现客户端负载均衡 Ribbon 实现客户端负载均衡 由于 Spring Cloud Ribbon 的封装, 我们在微服务架构中使用客户端负载均衡调用非常简单, 只需要如下两步: 1、启动多个服务提供者实例并…...
ChatGPT,乌合之众的疯狂
最近ChatGPT有多火爆就不用我说了。公司里,从CEO到技术人员,乃至于门口的保安、食堂的大婶,没有一个不会聊两句ChatGPT的。连我20年未见的小学同学、三线城市警官,都问我这东西能不能给领导写汇报材料。 用不了多久,家…...
代码随想录刷题-数组-长度最小的子数组
文章目录长度最小的子数组习题暴力解法滑动窗口长度最小的子数组 本节对应代码随想录中:代码随想录,讲解视频:拿下滑动窗口! | LeetCode 209 长度最小的子数组_哔哩哔哩_bilibili 习题 题目链接:209. 长度最小的子数…...
成功解决安装MySQL5.7提示公钥GPG密钥配置为file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
前言 大家好,我是沐风晓月,今天做MySQL5.7安装的时候遇到问题了,我们一起来复盘下这个问题,如果你使用我的方法没有解决,一定要留言给我,我们一起来排查和学习和完善。 本文收录于csdn 我是沐风晓月的专栏 【日常遇到的疑难问题和bug解决】 ,若点击无法跳转,请在csdn …...
vue配置环境变量
目录 创建配置文件 .env.development 文件 .env.production 文件 .env.dev 文件 使用变量 配置 package.json 文件 例子:在 api.js 使用 可以继续添加 创建配置文件 在根目录与 package.json 同级创建文件 .env.development、 .env.production、.env.dev 文件…...
js学习3(数组)
目录 结构图 数组操作 每日一练 结构图 数组操作 ## 数组中可以存储任何类型元素 ## 创建: 字面量([...])、创建对象(new Array(arr_len)) ## 遍历: 循环遍历、forEach(callback)、map(callback)、filter(callback)、every(callback)、some(callback)、…...
不用写代码也能开发,产品经理是怎么做到的?
产品经理再也不用求开发了……就在前几天,我做的小程序上线了! 从产品原型设计,前端开发后端开发,产品部署到运维,都是由我1个人完成的。 我是啥时候学会写代码的呢?不瞒你说,我一行代码都没写…...
Android源码分析 - Parcel 与 Parcelable
0. 相关分享 Android-全面理解Binder原理 Android特别的数据结构(二)ArrayMap源码解析 1. 序列化 - Parcelable和Serializable的关系 如果我们需要传递一个Java对象,通常需要对其进行序列化,通过内核进行数据转发,…...
数字孪生与 UWB 技术创新融合:从单点测量到全局智能化
人员定位是指利用各种定位技术对人员在特定场所的位置进行准确定位的技术。人员定位技术主要应用于需要实时监控、管理和保障人员安全的场所,如大型厂区、仓库、医院、学校、商场等。人员定位技术的应用范围非常广泛,例如:-在工厂生产线上&am…...
蓝桥杯嵌入式PWM_IN(打开中断)
1.原理图 2.配置 3.代码 关键函数 HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1) HAL_TIM_IC_CaptureCallback(TIM_HandTypeDef *htim)//回调函数 HAL_TIM_GET_COUNTER(&htim3) __HAL_TIM_SetCounter(&htim3,0)void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef …...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...
链式法则中 复合函数的推导路径 多变量“信息传递路径”
非常好,我们将之前关于偏导数链式法则中不能“约掉”偏导符号的问题,统一使用 二重复合函数: z f ( u ( x , y ) , v ( x , y ) ) \boxed{z f(u(x,y),\ v(x,y))} zf(u(x,y), v(x,y)) 来全面说明。我们会展示其全微分形式(偏导…...
门静脉高压——表现
一、门静脉高压表现 00:01 1. 门静脉构成 00:13 组成结构:由肠系膜上静脉和脾静脉汇合构成,是肝脏血液供应的主要来源。淤血后果:门静脉淤血会同时导致脾静脉和肠系膜上静脉淤血,引发后续系列症状。 2. 脾大和脾功能亢进 00:46 …...
CppCon 2015 学习:Simple, Extensible Pattern Matching in C++14
什么是 Pattern Matching(模式匹配) ❝ 模式匹配就是一种“描述式”的写法,不需要你手动判断、提取数据,而是直接描述你希望的数据结构是什么样子,系统自动判断并提取。❞ 你给的定义拆解: ✴ Instead of …...
