享元模式(Flyweight Pattern)。
定義
享元模式是池技術的重要實現方式,其定義如下:
使用共享物件可有效地支援大量的細粒度的物件。
享元模式的定義為我們提出了兩個要求:細粒度的物件和共享物件。我們知道分配太多的物件到應用程式中將有損程式的效能,同時還容易造成記憶體溢位,那怎麼避免呢?就是享元模式提到的共享技術。我們現來了解一下物件的內部狀態和外部狀態。
要求細粒度物件,那麼不可避免地使用物件數量多且性值相近,那我們就將這些物件的資訊分為兩個部分:內部狀態(intrinsic)與外部狀態(extrinsic)。
- 內部狀態
內部狀態是物件可共享的資訊,儲存在享元物件內部並且不會隨環境改變而改變,他們可以作為一個物件的動態符加資訊,不必直接儲存在具體某個物件中,屬於可以共享的部分。
-
外部狀態
外部狀態是物件得以依賴的一個標記,是隨環境改變而改變的、不可以共享的狀態,他是一批物件的統一標識,是唯一的一個所引致。 我們來看享元模式角色名稱。
-
Flyweight——抽象享元角色
他簡單的說就是一個產品的抽象類,同時定義出物件的外部狀態和內部狀態的介面或實現。
-
ConcreteFlyWeight——具體享元角色
具體的一個產品類,實現抽象角色定義的業務。該角色中需要注意的是內部狀態處理應該與環境無關,不應該出現一個操作改變了內部狀態,同時修改了外部狀態,這時絕對不允許的。
-
unsharedConcreteFlyweight——不可共享的享元角色
不存在外部狀態或者安全要求(如執行緒安全)不能夠使用共享技術的物件,該物件一般不會出現在享元工廠中。
-
FlyweightFactory——享元工廠
職責非常簡單,就是構造一個池容器,同時提供從池中獲得物件的方法。
通用原始碼
享元模式的目的在於執行共享技術,使得一些細粒度的物件可以共享,我們的設計確實應該這樣,多使用細粒度的的物件,便於重用或重構。我們來看享元模式的通用程式碼,先看抽象享元角色,如下所示。
public abstract class Flyweight { // 內部狀態 private String intrinsic; // 外部狀態 protected final String Extrinsic; /** * 要求共享元角色必須接受外部狀態 * * @param extrinsic */ public Flyweight(String extrinsic) { Extrinsic = extrinsic; } /** * 定義業務操作 */ public abstract void operate(); public String getIntrinsic() { return intrinsic; } public void setIntrinsic(String intrinsic) { this.intrinsic = intrinsic; } }
抽象享元角色一般為抽象類,在實際專案中,一般是一個實現類,他是描述一類事物的方法。在抽象角色中,一般需要把外部狀態和內部狀態(當然了,可以沒有內部狀態,只有行為也是可以的)定義出來,避免子類的隨意擴充套件。我們再來看具體的享元角色,如下所示。
public class ConcreteFlyweight1 extends Flyweight {
public ConcreteFlyweight1(String extrinsic) {
super(extrinsic);
}
@Override
public void operate() {
// 根據外部狀態進行邏輯處理
}
}
public class ConcreteFlyweight2 extends Flyweight {
public ConcreteFlyweight2(String extrinsic) {
super(extrinsic);
}
@Override
public void operate() {
// 根據外部狀態進行邏輯處理
}
}
這很簡單,實現自己的業務邏輯,然後接受外部狀態,以便內部業務邏輯對外部狀態的依賴。注意,我們在抽象享元中對外部狀態加上了final關鍵字,防止意外產生,什麼意外?獲得一個外部狀態,然後無意修改了一下,池就混亂了。
注意:在程式開發中,確認只需要一次賦值的屬性則設定為final型別,避免無意修改導致邏輯混亂,特別是Session級的常量或變數。
我們繼續看享元工廠,如下所示。
public class FlyweightFactory {
// 定義一個池容器
private static HashMap<String, Flyweight> pool = new HashMap<String, Flyweight>();
/**
* 享元工廠
*
* @param Extrinsic
* @return
*/
public static Flyweight getFlyweight(String Extrinsic) {
// 需要返回的物件
Flyweight flyweight = null;
// 在池中沒有該物件
if (pool.containsKey(Extrinsic)) {
flyweight = pool.get(Extrinsic);
} else {
// 根據外部狀態建立享元模式
flyweight = new ConcreteFlyweight1(Extrinsic);
// 放置到池中
pool.put(Extrinsic, flyweight);
}
return flyweight;
}
}
優點和缺點
享元模式是一個非常簡單的模式,他可以大大減少應用程式建立的物件,降低程式記憶體的佔用,增強程式的效能,但他同時也提高了系統複雜性,需要分離出外部狀態和內部狀態,而且外部狀態具有固話特性,不應該隨內部狀態改變而改變,否則導致系統邏輯混亂。
使用場景
- 系統中存在大量的相似物件。
- 細粒度的物件都具備較接近的外部狀態,而且內部狀態與環境無關,也就是說物件沒有特定身份。
- 需要緩衝池的場景。
擴充套件
執行緒安全的問題
只要使用Java開發,執行緒問題是不可避免的,那我們怎麼去避免這個問題呢?享元模式是讓我們使用共享技術,而Java的多執行緒又有如此問題,該如何設計呢?沒什麼可以參考的標準,只有依靠經驗,在需要的地方考慮一下執行緒安全,在大部分的場景下都不用考慮。我們在使用享元模式時,物件池中的享元物件儘量多,多到足夠滿足業務為止。
效能平衡
使用自己編寫的類作為外部狀態,必須覆寫equals方法和hashCode方法,而且執行效率還比較低,這彙總吃力不討好的事情最好別做,外部狀態最好以Java的基本型別作為標值,如String、int等,可以大幅的提升效率。
最佳實踐
Flyweight是拳擊比賽中的特定名詞,意思是“特輕量級”,指的是51公斤級比賽,用到設計模式中是指我們的類要輕量級,粒度要小,這才是他要表達的意思。粒度小了,帶來的問題就是物件太多,那就用共享技術來解決。
享元模式在Java API中也是隨處可見,如下面的程式就是一個很好的例子,如下所示。
public class Test {
public static void main(String[] args) {
String str1 = "和諧";
String str2 = "社會";
String str3 = "和諧社會";
String str4;
str4 = str1 + str2;
System.out.println(str3 == str4);
str4 = (str1 + str2).intern();
System.out.println(str3 == str4);
}
}
看看Java的幫助檔案中String類的intern方法。如果是String的物件池中有該型別的值,則直接返回物件池中的物件,那當然相等了。
需要說明一下的是,雖然可以使用享元模式可以實現物件池,但是這兩者還是有比較大的差異,物件池著重在物件的複用上,池中的每個物件是可替換的,從同一池中獲得A物件和B物件對客戶端來說是完全相同的,他主要解決複用,而享元模式在主要解決的物件的共享問題,如何建立多個可共享的細粒度物件則是其關注的重點。