享元模式(Flyweight Pattern)詳解
一、享元模式簡介
享元模式(Flyweight Pattern) 是一種 結構型設計模式(對象結構型模式),它通過共享技術實現相同或相似對象的重用,以減少內存占用和提高性能。具體來說,就是將一些細粒度的對象集中起來,使得這些對象可以互相共享。
又稱為輕量級模式,要求能夠被共享的對象必須是細粒度對象
運用共享技術有效地支持大量細粒度對象的復用。
簡單來說:
“如果你有一堆相似的對象,為什么不讓他們共享數據,從而節省內存呢?”
字符享元對象示意圖
享元模式:通過共享技術實現相同或相似對象的重用
享元池(Flyweight Pool):存儲共享實例對象的地方
原理
- 將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的
- 需要的時候將對象從享元池中取出,即可實現對象的復用
- 通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份
享元模式包含以下4個角色:
Flyweight(抽象享元類)
ConcreteFlyweight(具體享元類)
UnsharedConcreteFlyweight(非共享具體享元類)
FlyweightFactory(享元工廠類)
二、解決的問題類型
享元模式主要用于解決以下問題:
- 大量小對象導致內存消耗過大:當你的應用程序需要創建大量相似的小對象時,直接創建這些對象會導致內存使用過高。
- 提升性能:通過共享相同的對象實例來減少對象創建的數量,進而減少垃圾回收的壓力,提高系統性能。
- 對象的大部分狀態都可以外部化,可以將這些外部狀態傳入對象中
- 在使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,因此,在需要多次重復使用享元對象時才值得使用享元模式
三、使用場景
場景 | 示例 |
---|---|
字符串池 | Java 中的 String.intern() 方法 |
緩存管理 | 如緩存數據庫查詢結果 |
文檔處理 | 文本編輯器中的字符樣式管理 |
游戲開發 | 游戲中大量的相同紋理、模型等資源 |
四、核心概念
1. 內部狀態 vs 外部狀態
- 內部狀態:存儲在享元對象內部,可以在多個對象之間共享的部分,且不會隨環境改變而改變的狀態,通常是不變的數據。(例如:字符的內容)
- 外部狀態:不能被共享的部分,是隨環境變化而變化的數據。享元對象的外部狀態通常由客戶端保存,并在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。一個外部狀態與另一個外部狀態之間是相互獨立的(例如:字符的顏色和大小)
享元模式的核心思想就是分離對象的內部狀態和外部狀態,讓內部狀態可以共享,而外部狀態則由客戶端負責維護。
五、代碼案例(Java)
我們以一個簡單的文本編輯器為例,展示如何利用享元模式來優化字體樣式的管理。
1. 定義享元接口
interface CharacterStyle {void display(char character);
}
2. 創建具體的享元類
class CharacterStyleImpl implements CharacterStyle {private final String fontName;private final int fontSize;public CharacterStyleImpl(String fontName, int fontSize) {this.fontName = fontName;this.fontSize = fontSize;}@Overridepublic void display(char character) {System.out.println(character + " in " + fontName + ", size: " + fontSize);}
}
3. 創建享元工廠
class CharacterStyleFactory {private static final Map<String, CharacterStyle> styles = new HashMap<>();public static CharacterStyle getStyle(String fontName, int fontSize) {String key = fontName + "-" + fontSize;if (!styles.containsKey(key)) {styles.put(key, new CharacterStyleImpl(fontName, fontSize));}return styles.get(key);}
}
4. 使用享元模式
public class Client {public static void main(String[] args) {// 獲取樣式CharacterStyle style1 = CharacterStyleFactory.getStyle("Arial", 12);CharacterStyle style2 = CharacterStyleFactory.getStyle("Arial", 12);// 顯示字符style1.display('A'); // 輸出: A in Arial, size: 12style2.display('B'); // 輸出: B in Arial, size: 12// 驗證是否為同一實例System.out.println(style1 == style_Statics); // 輸出: true}
}
典型代碼(c++)
典型的抽象享元類代碼
abstract class Flyweight
{public abstract void Operation(string extrinsicState);
}
典型的具體享元類代碼
class ConcreteFlyweight : Flyweight
{//內部狀態intrinsicState作為成員變量,同一個享元對象其內部狀態是一致的private string intrinsicState;public ConcreteFlyweight(string intrinsicState) {this.intrinsicState = intrinsicState;}//外部狀態extrinsicState在使用時由外部設置,不保存在享元對象中,即使是同一個對象,在每一次調用時可以傳入不同的外部狀態public override void Operation(string extrinsicState) {//實現業務方法}
}
典型的非共享具體享元類代碼
class UnsharedConcreteFlyweight : Flyweight
{public override void Operation(string extrinsicState){//實現業務方法}
}
典型的享元工廠類代碼
using System.Collections;
class FlyweightFactory
{//定義一個Hashtable用于存儲享元對象,實現享元池private Hashtable flyweights = new Hashtable();public Flyweight GetFlyweight(string key){//如果對象存在,則直接從享元池獲取if (flyweights.ContainsKey(key)){return (Flyweight)flyweights[key];}//如果對象不存在,先創建一個新的對象添加到享元池中,然后返回else{Flyweight fw = new ConcreteFlyweight("state");flyweights.Add(key,fw);return fw;}}
}
其他案例
- 某軟件公司要開發一個圍棋軟件,其界面效果如下圖所示
該軟件公司開發人員通過對圍棋軟件進行分析發現,在圖中,圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作為一個獨立的對象存儲在內存中,將導致該圍棋軟件在運行時所需內存空間較大,如何降低運行代價、提高系統性能是需要解決的一個問題。為了解決該問題,現使用享元模式來設計該圍棋軟件的棋子對象。
如何讓相同的黑子或者白子能夠多次重復顯示但位于一個棋盤的不同地方?
解決方案:將棋子的位置定義為棋子的一個外部狀態,在需要時再進行設置
(引入外部狀態之后的圍棋棋子結構圖)
//Coordinates.cs
namespace FlyweightSample
{class Coordinates{private int x;private int y;public Coordinates(int x, int y){this.x = x;this.y = y;}public int X{get { return x; }set { x = value; }} public int Y{get { return y; }set { y = value; }}}
}
//IgoChessman.cs
using System;
namespace FlyweightSample
{abstract class IgoChessman{public abstract string GetColor();public void Display(Coordinates coord){Console.WriteLine("棋子顏色:{0},棋子位置:{1},{2}", this.GetColor(),coord.X,coord.Y); }}
}
- 共享網絡設備(無外部狀態)
很多網絡設備都是支持共享的,如交換機、集線器等,多臺終端計算機可以連接同一臺網絡設備,并通過該網絡設備進行數據轉發,如圖所示,現用享元模式模擬共享網絡設備的設計原理
- 共享網絡設備(有外部狀態)
雖然網絡設備可以共享,但是分配給每一個終端計算機的端口(Port)是不同的,因此多臺計算機雖然可以共享同一個網絡設備,但必須使用不同的端口。我們可以將端口從網絡設備中抽取出來作為外部狀態,需要時再進行設置
六、優缺點分析
優點 | 描述 |
---|---|
? 節省內存 | 減少了大量相似對象的創建,降低了內存使用 |
? 提高性能 | 減少了對象創建的時間開銷 |
? 支持大規模并發 | 在多線程環境中也能很好地工作 |
其他 | 外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元對象可以在不同的環境中被共享 |
缺點 | 描述 |
---|---|
? 復雜性增加 | 分離內部狀態和外部狀態增加了設計復雜度 |
? 可能影響可讀性 | 對于不熟悉該模式的人來說,理解代碼邏輯可能會變得困難 |
? 不適合頻繁變化的狀態 | 如果大部分狀態都是外部狀態,則享元模式的優勢無法體現。為了使對象可以共享,享元模式需要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間變長 |
七、與其他模式對比(補充)
模式名稱 | 目標 |
---|---|
單例模式 | 確保某個類只有一個實例,并提供全局訪問點 |
原型模式 | 通過克隆已有對象來創建新對象,強調復用 |
享元模式 | 通過共享技術減少相似對象的創建,節約內存 |
八、最終小結
享元模式是一種非常有效的設計模式,特別適合那些:
- 存在大量相似對象的應用場景;
- 希望減少內存使用并提高性能的情況;
- 可以明確區分內部狀態和外部狀態的對象。
掌握享元模式可以幫助你在處理大量相似對象時更加高效地管理內存資源,特別是在游戲開發、文檔處理等領域。
📌 一句話總結:
享元模式就像是一個“共享池”,把那些可以共享的部分放在一起,避免重復創建,從而達到節省內存的目的。
? 推薦使用方式:
- 當你發現你的應用中有許多相似但獨立的對象時,考慮使用享元模式。
- 注意合理劃分內部狀態和外部狀態,確保設計簡潔且易于維護。
九、擴展
單純享元模式和復合享元模式
單純享元模式:
所有的具體享元類都是可以共享的,不存在非共享具體享元類
復合享元模式:
將一些單純享元對象使用組合模式加以組合
如果希望為多個內部狀態不同的享元對象設置相同的外部狀態,可以考慮使用復合享元模式
部分內容由AI大模型生成,注意識別!