一、概述
一般問題:很多情況下需要在系統中增加大量相似對象的個數,從而將導致運行代價過高,性能下降。
核心方案:運用共享技術支持大量細粒度對象的復用,從而節約內存空間,提高系統性能。
設計意圖:首先,享元模式要求能夠共享的對象必須是細粒度對象——相似度高、狀態變化小。既然相似度高,從面向接口編程的思想出發,我們自然會想到先定義一個抽象享元類Flyweight;其次,享元模式的核心是復用已經存在的對象,減少了new的次數,那自然需要有個容器來管理這些對象,因此需要定義一個享元工廠類FlyweightFactory。
享元模式的一般設計圖如下:
FlyweightFactory本質上是一個對象池,現實中可以是自定義的工廠類,也可以是List、Stack等系統容器類。
共享維度:享元模式的核心是復用,而復用的對象又有粒度之分:
- 一種是復用整個享元對象,比如線程池、java字符串等,最終減少了對象創建和銷毀的次數。
- 另一種是將享元對象拆分為不變的內部狀態和可變的外部狀態,從而最大限度的復用其內部狀態,極大地減少了享元對象的個數。
二、復用享元對象
復用享元對象不難理解,我們以密碼輸入框中的圓點為例:
以CharState類來表示圓點,用戶每輸入一個字符都會新建一個CharState實例;同樣,用戶每刪除一個字符都會釋放一個CharState實例。而實際操作中,會出現反復的輸入和刪除字符,這樣頻繁的創建和銷毀實例必然增加系統開銷。
我們用享元模式來重新設計:
我們為CharState設計一個棧容器Stack<CharState>:
- 當輸入字符時:檢查棧是否為空,如果為空,則新建一個CharState實例;否則直接從棧中Pop一個CharState實例
- 當刪除字符時:直接將對應的CharState實例Push入棧
這樣,無論用戶如何操作,CharState實例的個數都不會超過密碼的最長位數。
對應代碼如下:
private CharState obtainCharState(char c) { CharState charState; //如果Pool為空則新建CharState實例,否則直接Pop一個實例 if(mCharPool.isEmpty()) { charState = new CharState(); } else { charState = mCharPool.pop(); charState.reset(); } charState.whichChar = c; return charState; }
三、復用享元內部狀態
相比復用享元對象,復用內部狀態更復雜一點。在享元模式中,可以共享的相同內容稱為內部狀態(Intrinsic State),而那些需要外部環境來設置的不能共享的內容稱為外部狀態(Extrinsic State),由於區分了內部狀態和外部狀態,因此可以通過設置不同的外部狀態使得相同的對象可以具有一些不同的特征,而相同的內部狀態是可以共享的。如下圖:
這里的重點是區分哪些是內部狀態,哪些是外部狀態。依然以密碼輸入框的圓點為例:
- 所有圓點的形狀、大小、顏色都是一樣的,這些都屬於內部狀態;
- 唯一不同的是各個圓點的位置,這是外部狀態。
也就是說,我們只需要一個CharState實例和一個存儲位置的外部列表就行了。
重新設計后的類圖如下:
- CharStateFactory負責緩存CharState實例,實際上只需要創建一個CharState實例
- Client維護了一個位置列表,代表各個圓點的位置
- CharState定義了Draw(int translationX)方法,在需要繪制的時候,由Client傳入位置信息
CharStateFactory代碼簡化為:
private CharState charState; public CharState obtainCharState(char c) { //如果charStete為null 則新建CharState實例 if(charState == null) { charState = new CharState(); } return charState; }
至此,由最開始每次輸入都要new一個CharState對象,到最多new密碼限制位數的CharState對象,再到只需要new一個CharState對象。可見,享元模式在消滅對象個數,節約內存方面的確效果顯著!
四、總結
優點:享元模式的優點在於它可以極大減少內存中對象的數量,使得相同對象或相似對象在內存中只保存一份。
缺點:享元模式使得系統更加復雜,需要分離出內部狀態和外部狀態,這使得程序的邏輯復雜化。
總結:享元模式是結構型設計模式,是一個考慮系統性能的設計模式,通過使用享元模式可以提高對象復用,節約內存空間,提高系統性能。