揭密设计模式:像搭乐高一样构建功能的装饰器模式

揭密设计模式:像搭乐高一样构建功能的装饰器模式

在软件开发中,我们常常会遇到一个问题:如何给一个对象动态地添加新功能,同时又不想修改它的代码?如果直接在原有类上修修补补,代码会变得臃肿复杂,难以维护。

今天,我们就来聊一个能完美解决这个问题的设计模式——装饰器模式 (Decorator Pattern)


什么是装饰器模式?

简单来说,装饰器模式允许你在不改变一个对象原有代码的前提下,为它增加新的职责或功能。 它就像是给一个基础对象“穿上”不同的“配件”,每件“配件”都代表一个新功能。


从一杯咖啡开始理解装饰器

为了更好地理解这个模式,我们来想象一下在咖啡店点咖啡的场景。

  1. 基础对象(Base Component) :首先,你有一杯最基础的黑咖啡(BlackCoffee) 。它有自己的价格(比如5元)和描述(“黑咖啡”)。
  2. 装饰器(Decorator) :现在你想给这杯咖啡加点东西,比如牛奶(Milk)和糖(Sugar) 。我们把这些配料看作是“装饰器”。它们本身也是咖啡的一种,只不过它们的作用是“包裹”另一杯咖啡,并在其基础上增加新的功能。

这个模式的关键在于,无论是黑咖啡还是牛奶、糖,它们都遵循同一个接口(Interface) 。这个接口定义了所有饮料都必须具备的行为,比如 getDescription()(获取描述)和 cost()(获取价格)。


装饰器模式的优势

  1. 符合开闭原则:我们不需要修改 BlackCoffee 类的代码。如果未来想增加新的配料,比如“奶油”,我们只需要新增一个 CreamDecorator 类即可,对原有代码完全无侵入。
  2. 避免“子类爆炸” :如果不用装饰器模式,我们可能需要创建 MilkCoffeeSugarCoffeeMilkSugarCoffee 等大量子类来应对不同的组合。这会让代码变得异常复杂。装饰器模式通过组合而非继承的方式,巧妙地解决了这个问题。
  3. 动态组合功能:用户可以根据需要,在运行时自由组合功能,比如先加奶再加糖,或者只加糖,非常灵活。

UML 类图

为了更直观地理解,我们来看一下装饰器模式的 UML 类图。

在这里插入图片描述

  • Beverage:定义了所有对象都必须实现的接口。
  • BlackCoffee:具体组件,提供了最基础的功能。
  • BeverageDecorator:抽象装饰器,继承了接口并持有一个接口的引用。
  • MilkDecoratorSugarDecorator:具体的装饰器,用来添加新功能。

装饰器模式在框架中的应用

除了我们自己手写的代码,装饰器模式在许多成熟的框架中都有广泛应用。

Java I/O 流

Java 的 I/O 流库是装饰器模式最经典的例子之一。InputStreamOutputStream 是最基础的接口。而像 BufferedInputStreamDataInputStreamGZIPInputStream 等类,都是装饰器。它们通过包裹一个基础的 InputStream,为其添加新的功能,比如提供缓冲、处理基本数据类型、文件解压缩等。

Spring 框架

在 Spring 框架中,装饰器模式与代理模式的思想常常结合使用。当一个 Bean 被 Spring AOP 增强时,Spring 会创建一个代理对象。这个代理对象实际上就是装饰器,它包裹着原始的 Bean 对象。当方法被调用时,代理对象会先执行一些额外的逻辑(比如事务的开启和提交、日志的记录),然后再将调用转发给原始的 Bean 对象。这种设计使得 Spring 可以在不修改原始业务代码的情况下,为其动态地添加横切关注点(Cross-cutting Concerns),完美体现了装饰器模式的精髓。


装饰器模式与相似模式的对比

最后,为了更精确地理解装饰器模式,我们来把它和两个容易混淆的模式进行比较。

装饰器模式 vs 代理模式
  • 目的不同:装饰器模式的目的是动态地增强一个对象的功能。而代理模式的目的是控制对一个对象的访问
  • 联系与区别:虽然两者在结构上相似(都持有一个对目标对象的引用),但其意图不同。在某些情况下(如 Spring AOP),代理既可以作为访问控制的手段,也可以作为功能增强的方式,从而模糊了两者之间的界限。
装饰器模式 vs 组合模式
  • 目的不同:装饰器模式是为了增强一个单一对象的功能。而组合模式是为了将对象组织成树形结构,以表示“部分-整体”的层次关系。
  • 结构不同:在装饰器模式中,装饰器和被装饰者都实现同一个接口。而在组合模式中,组合对象和叶子对象也实现同一个接口,但组合对象内部持有的是多个接口的引用(一个集合),目的是管理子对象。

Java 代码实现

接下来,我们用 Java 来实现这个咖啡店的例子。

1. 统一接口:Beverage

首先,定义所有饮料都必须实现的接口。

// Beverage.java
public interface Beverage {String getDescription();double cost();
}
2. 具体组件:BlackCoffee

然后,我们创建最基础的饮料类,它实现了 Beverage 接口。

// BlackCoffee.java
public class BlackCoffee implements Beverage {@Overridepublic String getDescription() {return "黑咖啡";}@Overridepublic double cost() {return 5.0;}
}
3. 抽象装饰器:BeverageDecorator

为了让所有装饰器都具有统一的结构,我们创建一个抽象装饰器类。它也实现了 Beverage 接口,并持有一个对 Beverage 对象的引用。所有的具体装饰器都将继承这个抽象类。

// BeverageDecorator.java
public abstract class BeverageDecorator implements Beverage {protected Beverage beverage;public BeverageDecorator(Beverage beverage) {this.beverage = beverage;}@Overridepublic abstract String getDescription();@Overridepublic abstract double cost();
}
4. 具体装饰器:MilkDecorator 和 SugarDecorator

现在,我们创建具体的装饰器类。它们继承 BeverageDecorator 并重写方法,在原有功能上添加新的职责。

// MilkDecorator.java
public class MilkDecorator extends BeverageDecorator {public MilkDecorator(Beverage beverage) {super(beverage);}@Overridepublic String getDescription() {// 在原有描述上添加 "加奶"return beverage.getDescription() + ",加奶";}@Overridepublic double cost() {// 在原有价格上加上牛奶的费用return beverage.cost() + 3.0;}
}
// SugarDecorator.java
public class SugarDecorator extends BeverageDecorator {public SugarDecorator(Beverage beverage) {super(beverage);}@Overridepublic String getDescription() {// 在原有描述上添加 "加糖"return beverage.getDescription() + ",加糖";}@Overridepublic double cost() {// 在原有价格上加上糖的费用return beverage.cost() + 1.0;}
}
5. 客户端代码:如何使用

最后,我们来看看如何将这些组件组合起来,构造出我们想要的咖啡。

// Main.java
public class Main {public static void main(String[] args) {// 1. 来一杯纯黑咖啡Beverage blackCoffee = new BlackCoffee();System.out.println("描述: " + blackCoffee.getDescription() + ",价格: " + blackCoffee.cost());// 2. 来一杯加奶的黑咖啡Beverage milkCoffee = new MilkDecorator(blackCoffee);System.out.println("描述: " + milkCoffee.getDescription() + ",价格: " + milkCoffee.cost());// 3. 来一杯加奶又加糖的黑咖啡Beverage milkSugarCoffee = new SugarDecorator(milkCoffee);System.out.println("描述: " + milkSugarCoffee.getDescription() + ",价格: " + milkSugarCoffee.cost());}
}

运行结果:

描述: 黑咖啡,价格: 5.0
描述: 黑咖啡,加奶,价格: 8.0
描述: 黑咖啡,加奶,加糖,价格: 9.0

结语

装饰器模式是一种强大且灵活的设计模式。它通过“包裹”而非修改的方式,让我们可以像搭乐高积木一样,动态地为对象添加和组合功能。当你的系统需要灵活扩展、避免大量子类时,不妨考虑一下这个精妙的模式。

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

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

相关文章

【Vue】前端 vue2项目搭建入门级(二)

本文不同于【Vue】前端 vue2项目搭建入门级(一),本文创建vue2项目方式是一键创建vue2 项目,不需要自己配置。1.cmd进入根目录,输入vue create project(vue create 项目名)创建一个project的项目…

基于SQLite索引的智能图片压缩存储系统设计与实现

摘要 本文介绍一种基于SQLite索引的智能图片压缩存储系统,通过融合图像质量压缩与数据压缩技术,实现60-80%的压缩率,较传统方法压缩效率提升4-5倍。系统采用“大文件存储索引数据库”架构,针对性解决海量图片数据迁移与存储中的核…

【一张图看懂Kafka消息队列架构】

一张图看懂Kafka消息队列架构Kafka架构全景图ApacheKafka作为当今最流行的分布式消息队列系统,其架构设计精巧而高效。通过一张典型的Kafka架构图,我们可以清晰地看到几个核心组件:生产者(Producer)、消费者(Consumer)、主题(Topic)、分区(Pa…

计算机三级嵌入式填空题——真题库(24)原题附答案速记

1.表征数字音频每秒钟数据量的参数称为波形声音的__码率__。CD音乐的声音信号的采样率约为44kHz,量化位数为16位,采用双声道,则该参数的值为__1408__kb/s。(码率取样频率*量化位数*声道数44kHz*16*21408kb/s)2.利用载波…

Gradle vs. Maven,Java 构建工具该用哪个?

Java构建工具的甜咸粽子之争,就是 Gradle 和 Maven 该用哪个? 随心所欲的手动挡 vs. 稳如老狗的自动挡 Maven用的是pom.xml。很多人一听XML就头大,觉得又臭又长。但换个角度想,XML的缺点正是它最大的优点:死板、规范、…

将Markdown文档输出成Word格式

大家好!今天想和大家分享一个技术文档格式转换的小故事。有个朋友在软件行业从事文档工作,她们的手册是用Markdown编写的,使用Facebook的Docsaurus框架,在线浏览很方便,但输出Word格式却很不方便,问我是否有…

COMSOL基于Voronoi毛细管及多边形骨料ITZ的微介观混凝土水分扩散模型

本案例是通过COMSOL对论文An innovative method for mesoscale modelling of moisture diffusion in concrete(https://doi.org/10.1016/j.cemconcomp.2024.105836)中Voronoi毛细管、多边形骨料、ITZ、水泥浆体多相材料的几何模型复现。 其中论文中的混…

机器学习和高性能计算中常用的几种浮点数精度

浮点数 (Floating-Point Number) 是一种在计算机中表示带有小数部分的数字的方式。它通过科学记数法类似的方式(尾数 基数 ^ 指数)来近似表示实数。浮点数的精度决定了它可以表示的数值范围以及数值之间的精细程度。 常见的浮点数精度包括:F…

开源大语言模型(Qwen3)

Qwen3是阿里巴巴达摩院于2025年4月29日发布的新一代开源大语言模型,属于通义千问系列的最新成员。其核心突破在于首创混合推理架构,将人类认知科学中的“快思考”与“慢思考”机制融入模型设计,实现了复杂任务处理与高效响应的平衡。 一、技术…

懒人精灵本地离线卡密验证系统教程(不联网、安全稳定、省钱、永久免费、无任何限制)

1.合集懒人精灵本地离线卡密验证系统教程(不联网、安全稳定、省钱、永久免费、无任何限制):https://www.bilibili.com/video/BV1B5PjeGETQ/ 备注: 1.本地离线卡密采用最安全的非对称加解密技术,设备id采用最安全多重混合加密不可逆技术生成,验证阶段需要网络时间,内置防抓…

【三维渲染技术讨论】Blender输出的三维文件里的透明贴图在Isaac Sim里会丢失, 是什么原因?

Blender导出的三维文件在Isaac Sim中丢失透明贴图,通常与文件格式兼容性、材质属性映射、导出设置或Isaac Sim材质解析逻辑有关。以下是具体原因分析和解决方法: 一、可能的原因文件格式对透明信息的支持差异 Blender常用的导出格式(如FBX、G…

Java线程池深度解析:从原理到实战的完整指南

Java线程池深度解析:从原理到实战的完整指南 🌟 你好,我是 励志成为糕手 ! 🌌 在代码的宇宙中,我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光,在逻辑的土壤里生长成璀璨的…

机器学习——模型架构

有监督学习 线性模型 多元线性回归:预测连续的数值(如房价、销量)。 逻辑回归:解决二分类问题(如判断邮件是否是垃圾邮件),输出概率。 非线性模型 决策树:通过一系列if-then规则进行…

深入理解Kafka事务

一 kafka事务介绍1.1 Kafka事务的作用Exactly-Once Semantics (EOS):在“消费 → 处理 → 生产”的流式链路里避免重复写与重复读带来的副作用,确保“处理一次且仅一次”的可见效果。跨分区 / 跨 Topic 原子性:将一次处理内写入的多分区多主题…

RabbitMinQ(模拟实现消息队列项目)

目录 一.消息队列背景 二.需求分析 核心概念: BrokerServer: BrokerServer的核心API: 交换机Exchange: 持久化: 网络通信: 消息应答: 三、模块划分 四、创建项目 五、创建核心类 Exchange: MSGQueue: Binding: Message: 六.…

如何构建StarRocks官方文档

不知道是网络问题还是官网问题,StarRocks文档经常出现卡顿的情况,曾经构建过Flink文档, 所以也想尝试自己构建一个StarRocks的本地官方文档 断断续续折腾了好几天,就不废话了,直接上实际步骤 1. 环境 1.1 Linux环境 …

堡垒机(跳板机)入门指南:构建更安全的多服务器运维架构

随着你的业务不断扩张,你云上服务器的数量,是不是也从一台,变成了三台、五台、甚至一个由几十台机器组成的庞大集群?你像一个尽职的“国王”,为你王国的每一座“城池”(每一台服务器)&#xff0…

(链表)Leetcode206链表反转+Leetcode6删除链表的倒数第N个结点+虚拟头节点使用

虚拟头结点的作用是:简化插入/删除逻辑方便返回头节点减少边界错误 Leetcode206链表反转 206. 反转链表 - 力扣(LeetCode) 头插法 # Definition for singly-linked list. # class ListNode(object): # def __init__(self, val0, nextN…

自然语言处理NLP:嵌入层Embedding中input_dim的计算——Tokenizer文本分词和编码

1. 词汇表大小(input_dim)计算方法 嵌入层Embedding中的input_dim是根据数据中所有唯一词(或字)的总数来决定的。可以通过Tokenizer文本分词和编码得到。 简单说,Tokenizer 是一个文本分词和编码器,它主要做…

python中的分代垃圾回收机制的原理【python进阶二、2】

1. 分代设计思想Python 将对象按存活时间分为三代(Generation 0, 1, 2):0代(年轻代):新创建的对象。1代(中年代):经历一次GC扫描后存活的对象。2代(老年代&am…