揭密設計模式:像搭樂高一樣構建功能的裝飾器模式

揭密設計模式:像搭樂高一樣構建功能的裝飾器模式

在軟件開發中,我們常常會遇到一個問題:如何給一個對象動態地添加新功能,同時又不想修改它的代碼?如果直接在原有類上修修補補,代碼會變得臃腫復雜,難以維護。

今天,我們就來聊一個能完美解決這個問題的設計模式——裝飾器模式 (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
英文地址,請注明出處:http://en.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…