享元模式 FlyWeight 結構型 設計模式(十五)


享元模式(FlyWeight) 
”取“共享”之意,“”取“單元”之意。
image_5c087c0a_b27

意圖

運用共享技術,有效的支持大量細粒度的對象。

意圖解析

面向對象的程序設計中,一切皆是對象,這也就意味着系統的運行將會依賴大量的對象。
試想,如果對象的數量過多,勢必會增加系統負擔,導致運行的代價過高。
下面看兩個小例子理解下
1.)有一首歌曲叫做《大舌頭》
其中有一句歌詞“說說說說 說你愛我 我我我我 說不出口”
image_5c087c0a_3c7e
如果使用面向對象的編程方式對這段歌詞進行描述,假設一個漢字表示一個對象,你會怎么做?
你會用七個還是十六個對象進行表示?
image_5c087c0a_1241
 
2.)有一個文本編輯器軟件,對於每一個字符使用對象進行表示
當打開一篇有很多重復字符的、數萬字的文章時,你會使用幾個對象進行表示?
如果仍舊采用每個字符占用一個對象,系統勢必崩潰,必然需要共享對象
 
上面的兩個例子中,都涉及到重復對象的概念 
而享元模式的意圖就是如此,將 重復的對象進行共享以達到支持大量細粒度對象的目的
如果不進行共享,如例2中描述的那樣,一篇數萬字符的文章將會產生數萬個對象,這將是一場可怕的災難。
image_5c087c0a_7394
flyweight意為輕量級
在我們當前的場景下,寓意為通過共享技術,輕量級的---也就是內存占用更小
本質就是“共享”所以中文翻譯過來多稱之為享元
簡言之,享元模式就是要“共享對象”
對於Java語言,我們熟悉的String,就是享元模式的運用
String是不可變對象,一旦創建,將不會改變
在JVM內部,String對象都是共享的
如果一個系統中的兩個String對象,包含的字符串相同,只會創建一個String對象提供給兩個引用
從而實現String對象的共享(new 的對象是兩個不同的)
image_5c087c0a_149c
享元模式又不僅僅是簡單的“共享對象”
上面的兩個小例子中,對於文字中的重復字符
可以通過共享對象的方式,對某些對象進行共享,從而減少內存開銷。
考慮下圖中的情景,這里面所有的“你”字,到底是不是同樣的?
  • 是,因為全部都是漢字“你”
  • 不是,因為盡管都是漢字“你”,但是他們的字體,顏色,字號,卻又明顯不同,所以不是同樣的
image_5c087c0b_388b
 
如果將字體、顏色、字號,作為“你”這個漢字的狀態
是不是可以認為:他們都是一樣的漢字,但是他們卻又具有不同的狀態?
其實享元模式不僅僅用來解決大量重復對象的共享問題,還能夠用來解決相似對象的問題。
享元對象能夠共享的關鍵在於:區分對象的 內部狀態外部狀態
內部狀態是存儲在享元對象內部的,並且不會隨環境的變化而有所改變。
比如上面的漢字“你”,無論在任何情況下,漢字“你”,始終是“你”,不會變成“她”
所以說享元模式解決共享問題,本質是共享內部狀態
外部狀態是隨外部環境變化而變化,不能共享的狀態。
享元對象的外部狀態通常由客戶端保存,在必要的時候在傳遞到享元對象內部
比如上面漢字“你”的字體、顏色、字號就是外部狀態。   

小結

享元模式就是為了避免系統中出現大量相同或相似的對象,同時又不影響客戶端程序通過面向對象的方式對這些對象進行操作
享元模式通過共享技術,實現相同或相似對象的重用
比如文編編輯器讀取文本
在邏輯上每一個出現的字符都有一個對象與之對應,然而在物理上它們卻共享同一個享元對象
在享元模式中,存儲這些共享實例對象的地方通常叫做享元池(Flyweight  Pool)
享元模式可以結合String的 intern()方法一起進行理解
 
通過區分了內部狀態和外部狀態,就可以將相同內部狀態的對象存儲在池中,池中的對象可以實現共享
需要的時候將對象從池中取出,實現對象的復用
通過向取出的對象注入不同的外部狀態,進而得到一些列相似的對象
而這些看似各異的對象在內存中,僅僅存儲了一份,大大節省了空間,所以說很自然的命名為“flyweight”輕量級

享元工廠

通過對意圖的認識,可以認為, 享元模式其實就是對於“程序中會出現的大量重復或者相似對象”的一種“重構”
當然,你應該是在設計之初就想到這個問題,而不是真的出現問題后再去真的重構
比如,你想要設計“字符”這種對象時,就應該考慮到他的“大量””重復““相似”的特點
所以需要分析出字符的內部狀態,與外部狀態
上面也提到對於享元對象,通過享元池進行管理
對於池的管理通常使用工廠模式,借助於工廠類對享元池進行管理
用戶需要對象時,通過工廠類獲取
工廠提供一個存儲在享元池中的已創建的對象實例,或者創建一個新的實例

示例代碼

針對於上面的例子,漢字“你”作為內部狀態,可以進行共享
“顏色”作為外部狀態,由客戶端保存傳遞
創建字符類 Character、漢字字符類ChineseCharacter、顏色類Color以及工廠類CharacterFactory
Color含有顏色屬性,通過構造方法設置,getter方法獲取
package flyweight;
public class Color {
    public String Color;
    public Color(String color) {
        this.Color = color;
    }
    public String getColor() {
        return Color;
    }
}

 

Character 抽象的字符類,用於描述字符
package flyweight;
public abstract class Character {
    public abstract String getValue();
     
    public void display(Color color) {
        System.out.println("字符: " + getValue() + " ,顏色: " + color.getColor());
    }
}
漢字字符類,為了簡化,直接設置value為漢字“你”

 

package flyweight;
public class ChineseCharacter extends Character {
    @Override
    public String getValue() {
        return "你";
    }
}
CharacterFactory字符工廠類
通過單例模式創建工廠
內部HashMap用於存儲字符,並且提供獲取方法
為了簡化程序,初始就創建了一個漢字字符“你”存儲於字符中
package flyweight;
import java.util.HashMap;
public class CharacterFactory {
    /**
    * 單例模式 餓漢式創建
    */
    private static CharacterFactory instance = new CharacterFactory();
    /**
    * 使用HashMap管理享元池
    */
    private HashMap<String, Object> hm = new HashMap<>();
    private CharacterFactory() {
        Character character = new ChineseCharacter();
        hm.put("你", character);
    }
    /**
    * 單例全局訪問接口獲取工廠
    */
    public static CharacterFactory getInstance() {
        return instance;
    }
     
    /**
    * 根據key獲取池中的對象
    */
    public Character getCharacter(String key) {
        return (Character) hm.get(key);
    }
}
測試代碼
image_5c087c0b_6037
示例中,我們通過工廠,從享元池中獲取了三個漢字字符“你”。
通過 == 可以看得出來,他們都是同一個對象
在分別調用他們的display方法時,在客戶端(此處為我們的Test main方法)中創建,並且傳遞給享元對象
通過方法參數的形式進行外部狀態的設置。
image_5c087c0b_2137
 
CharacterFactory 單例模式,返回自身實例
CharacterFactory內部維護Character的享元池
Character 依賴Color
ChineseCharacter是Character的實現類

結構

將上面的示例轉換為標准的享元模式的名稱
image_5c087c0b_1edf
 
抽象享元角色 FlyWeight
所有具體享元類的超類,為這些類規定了需要實現的公共接口
外部狀態可以通過業務邏輯方法的參數形式傳遞進來
具體享元角色ConcreteFlyWeight
實現抽象享元角色所規定的的接口
需要保存內部狀態,而且,內部狀態必須與外部狀態無關
從而才能使享元對象可以在系統內共享
享元工廠角色 FlyWeightFactory
負責創建和管理享元角色,也就是維護享元池
必須保證享元對象可以被系統適當的共享
接受客戶端的請求
如果有適當符合要求的享元對象,則返回
如果沒有一個適當的享元對象,則創建
客戶端角色Client
客戶端角色維護了對所有享元對象的引用
image_5c087c0b_69dd
需要保存維護享元對象的外部狀態,然后通過享元對象的業務邏輯方法作為參數形式傳遞
image_5c087c0b_6b32

分類

單純享元模式

在上面的結構中,如果所有的ConcreteFlyWeight都可以被共享
也就是所有的FlyWeight子類都可以被共享,那就是所有的享元對象都可以被共享
這種形式被稱之為 單純享元模式
單純享元代碼
package flyweight.simple;
public abstract class FlyWeight {
/**
* 抽象的業務邏輯方法,接受外部狀態作為參數
*/
abstract public void operation(String outerState);
}
package flyweight.simple;
public class ConcreteFlyWeight extends FlyWeight {
    private String innerState = null; 
    public ConcreteFlyWeight(String innerState) {
        this.innerState = innerState;
    } 
    /**
    * 外部狀態作為參數傳遞
    */
    @Override
    public void operation(String outerState) {
        System.out.println("innerState = " + innerState + " outerState = " + outerState);
    }
}
package flyweight.simple;
import java.util.HashMap;
public class FlyWeightFactory {
    /**
    * 單例模式 餓漢式創建
    */
    private static FlyWeightFactory instance = new FlyWeightFactory();
    /**
    * 使用HashMap管理享元池
    */
    private HashMap<String, Object> hm = new HashMap<>();
     
    private FlyWeightFactory() {
    }
    /**
    * 單例全局訪問接口獲取工廠
    */
    public static FlyWeightFactory getInstance() {
    return instance;
    }
    /**
    * 根據innerState獲取池中的對象
    * 存在返回,不存在創建並返回
    */
    public FlyWeight getFylWeight(String innerState) {
        if(hm.containsKey(innerState)){
        return (FlyWeight) hm.get(innerState);
        }else{
        FlyWeight flyWeight = new ConcreteFlyWeight(innerState);
        hm.put(innerState,flyWeight);
        return flyWeight;
        }
    }
}

 

package flyweight.simple;
public class Test {
public static void main(String[] args){
FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance();
FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First");
FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second");
FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First");

System.out.println(flyWeight1);
System.out.println(flyWeight2);
System.out.println(flyWeight3);
System.out.println();

flyWeight1.operation("outer state XXX");
flyWeight2.operation("outer state YYY");
flyWeight3.operation("outer state ZZZ");
}
}

 

 
image_5c087c0b_75b6   

復合享元模式

與單純享元模式對應的是復合享元模式
單純享元模式中,所有的享元對象都可以共享
復合享元模式中,則並不是所有的ConcreteFlyWeight都可以被共享
也就是說: 不是所有的享元對象都可以被共享
實際上,並不是所有的FlyWeight子類都需要被共享
FlyWeight接口使的可以進行共享,但是沒有任何必要 強制必須共享
實踐中,UnsharedConcreteFlyWeight對象通常將ConcreteFlyWeight對象作為子節點
image_5c087c0b_4a4f
與單純享元模式相比,僅僅是擁有了不可共享的具體子類
而且,這個子類往往是應用了組合模式,將ConcreteFlyWeight對象作為子節點
復合享元角色UnsharedConcreteFlyWeight
復合享元角色,也就是不可共享的,也被稱為 不可共享的享元對象
但是一個復合享元對象可以分解為多個本身是單純享元對象的組合
這些單純的享元對象就又是可以共享的
 
復合享元代碼
將簡單模式中的示例代碼進行改造
FlyWeight不變
package flyweight.composite;
public abstract class FlyWeight {
/**
* 抽象的業務邏輯方法,接受外部狀態作為參數
*/
abstract public void operation(String outerState);
}
ConcreteFlyWeight不變
package flyweight.composite;
public class ConcreteFlyWeight extends FlyWeight {
private String innerState = null;
public ConcreteFlyWeight(String innerState) {
this.innerState = innerState;
}
/**
* 外部狀態作為參數傳遞
*/
@Override
public void operation(String outerState) {
System.out.println("innerState = " + innerState + " outerState = " + outerState);
}
}
新增加不共享的子類也就是組合的享元子類
內部使用list 維護單純享元模式對象,提供add方法進行添加
提供operation操作
package flyweight.composite;
import java.util.ArrayList;
import java.util.List;
public class UnsharedConcreateFlyWeight extends FlyWeight {
private String innerState = null;
public UnsharedConcreateFlyWeight(String innerState) {
this.innerState = innerState;
}

private List<FlyWeight> list = new ArrayList<>();
public void add(FlyWeight flyWeight) {
list.add(flyWeight);
}
@Override
public void operation(String outerState) {
for (FlyWeight flyWeight:list) {
flyWeight.operation(outerState);
    }
}
}
FlyWeightFactory工廠類進行改造
新增加public UnsharedConcreateFlyWeight getCompositeFylWeight(String state) 
用於獲得組合享元對象
package flyweight.composite;
 
import java.util.HashMap;
 
public class FlyWeightFactory {
    /**
    * 單例模式 餓漢式創建
    */
    private static FlyWeightFactory instance = new FlyWeightFactory();
     
    /**
    * 使用HashMap管理享元池
    */
    private HashMap<String, Object> hm = new HashMap<>();
     
    /**
    * 管理復合享元對象
    */
    private HashMap<String, Object> compositeHm = new HashMap<>();
     
    private FlyWeightFactory() {
    }
     
    /**
    * 單例全局訪問接口獲取工廠
    */
    public static FlyWeightFactory getInstance() {
        return instance;
    }
     
    /**
    * 根據innerState獲取池中的對象
    * 存在返回,不存在創建並返回
    */
    public FlyWeight getFylWeight(String innerState) {
        if(hm.containsKey(innerState)){
            return (FlyWeight) hm.get(innerState);
        }else{
            FlyWeight flyWeight = new ConcreteFlyWeight(innerState);
            hm.put(innerState,flyWeight);
            return flyWeight;
        }
    }
     
    /**
    * 根據innerState獲取池中的對象
    * 存在返回,不存在創建並返回
    */
    public UnsharedConcreateFlyWeight getCompositeFylWeight(String state) {
        if(compositeHm.containsKey(state)){
            return (UnsharedConcreateFlyWeight) compositeHm.get(state);
        }else{
            UnsharedConcreateFlyWeight flyWeight = new UnsharedConcreateFlyWeight(state);
            compositeHm.put(state,flyWeight);
            return flyWeight;
        }
    }
 
}
測試類也進行改造
package flyweight.composite;
public class Test {
public static void main(String[] args){
    FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance();
    FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First");
    FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second");
    FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First");
     
    System.out.println(flyWeight1);
    System.out.println(flyWeight2);
    System.out.println(flyWeight3);
     
    System.out.println("###########################################");
     
    flyWeight1.operation("outer state XXX");
    flyWeight2.operation("outer state YYY");
    flyWeight3.operation("outer state ZZZ");
    System.out.println("###########################################");
    UnsharedConcreateFlyWeight compositeFlyWeight = flyWeightFactory.getCompositeFylWeight("composite");
    compositeFlyWeight.add(flyWeight1);
    compositeFlyWeight.add(flyWeight2);
    compositeFlyWeight.operation("composite out state OOO");
    }
}
image_5c087c0b_5108
 
測試程序在原來的基礎上,新獲得了一個組合享元對象
然后將兩個單純享元對象添加到組合享元對象中
然后調用operation,通過打印信息可以看得出來
不同的單純享元對象,他們卻有了一致的外部狀態
image_5c087c0b_4c7c
 
所以使用復合享元模式的一個常用目的就是:
多個內部狀態不同的單純享元對象,擁有一致的外部狀態
這種場景下,就可以考慮使用復合享元模式

使用場景

如果有下列情況,則可以考慮使用享元模式
  • 應用程序中使用了大量的對象
  • 大量的對象明顯增加了程序的存儲運行開銷
  • 對象可以提取出內部狀態,並且可以分離外部狀態
使用享元模式有一點需要特別注意:應用程序運行不依賴這些對象的身份
換句話說這些對象是不做區分的,適用於“在客戶端眼里,他們都是一樣的”這種場景
比如單純的使用對象的方法,而不在意對象是否是創建而來的,否則如果客戶端鑒別對象的身份(equals),當他們是同一個對象時將會出現問題  

總結

享元模式的核心就是共享
共享就需要找准內部狀態,以及分離外部狀態外部狀態由客戶端維護,在必要時候,通過參數的形式注入到享元對象中
在有大量重復或者相似對象的場景下,都可以考慮到享元模式
 
而且為了達到共享的目的,需要通過 工廠對象進行控制
只有通過工廠來維護享元池才能達到共享的目的,如果任意創建使用則勢必不能很好地共享
 
享元模式大大的減少了對象的創建,降低了系統所需要的內存空間
但是由於 將狀態分為內部狀態和外部狀態,而外部狀態是分離的,那么狀態的讀取必然會增大開銷
所以說 享元模式是時間換空間
 
如果確定需要使用享元模式,如果對於多個內部狀態不同的享元對象,希望他們擁有一致性的外部狀態
那么就可以考慮復合享元模式,復合享元模式是與合成模式的結合。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM