1. 程式人生 > >享元模式(Flyweight Pattern)。

享元模式(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物件對客戶端來說是完全相同的,他主要解決複用,而享元模式在主要解決的物件的共享問題,如何建立多個可共享的細粒度物件則是其關注的重點。