天天看點

Flyweight(享元)模式

Flyweight模式可實作客戶代碼之間的對象共享,建立共享對象的職責,這一點普通對象是不具備的。一般的對象不必關心共享職責,任何時刻最多隻能有一個客戶代碼引用它,其他時刻可以是其他客戶代碼引用。如果多個客戶代碼引用同一個對象,那麼當某個客戶代碼修改了該對象的狀态時,該對象是不會通知其他客戶代碼的。然而,有時候,我們需要讓多個客戶代碼共享通路同一個對象。

    當我們必須管理成千上萬個小型對象的時候,例如線上電子圖書中的字元對象,我們需要讓多個客戶代碼共享一個對象。在這種情況下,為了提高應用程式的性能,需要考慮這些細粒度的對象在多個客戶代碼之間的安全共享通路問題。對于線上電子圖書來說,一本書隻需要一個A對象,但是當出現不同的A對象時,則需要我們采取某種方法對之模組化。

   Flyweight模式的主要意圖是:通過共享來支援大量的細粒度對象的使用效率。

1. 不變性:

  Flyweight模式可讓多個客戶安全共享對有限數量對象的通路。為實作這個目标,我們必須考慮如果某個客戶代碼改變了共享對象的狀态,這将會影響共享該對象的其他所有客戶代碼。在最普通的情況下,該對象僅被一個單獨的客戶代碼通路,自然不存在任何問題。當多個客戶代碼共享通路一個對象的時候,如何避免客戶代碼之間的互相影響是個值得關注的問題,這個問題最簡單和最常見的解決辦法,便是限制客戶代碼調用任何可能引起共享對象狀态變化的方法。在建立對象時,可以将這個對象定義為immutable類型這樣該對象就不會被改變。Java語言中不可變對象是String類對象。一旦建立了一個String對象,任何客戶代碼都無法改變它的字元。

突破題:你是否認同Java語言設計者将String對象定義為不可變的做法,并請給出你的理由。

答:贊成方觀點,主要是考慮到實際應用中,字元串通常被多個使用者共享。如果字元串能夠被修改,那麼一個使用者對字元串不經意的改動就會影響其他使用者。這種問題在實際應用中經常會出現,它的根結在于字元串可能會被修改。例如,當一個方法傳回一個Customer對象的客戶名字元串之後,它仍然保留了對該字元串的引用。如果字元串可以被修改,那麼當使用者在散清單中将該字元串變成全大寫的形式,Customer對象的名字也會随之改變。在Java語言中,你可以把某字元串全部大寫,但必須使用新對象,而不是僅僅修改原來的對象。字元串的不變性有利于多個使用者安全地共享該字元串。更進一步說,字元串的不可變性也可以防止系統出現安全危機。

     反對方觀點:将字元串設定為不變,這樣做的确能夠避免我們犯某些錯誤,但是其負面影響也很大。首先,即使在确實需要修改的場合,開發人員也無法修改字元串。第二,在一種計算機語言中加入特殊的規則,将會使該語言難以學習和使用。Java語言和Smalltalk語言功能一樣強大,但是前者就比後者難學多了。最後,任何語言都無法避免使用者犯錯誤。如果一種計算機語言簡單易學,使用者就能夠更加深入地掌握它,這樣使用者就會有更多的時間來研究如何建構和使用該語言的測試架構。

     如果有大量的類似對象需要管理,也許需要共享這些對象;不過,它們可能不是不可變的。在這種情況下,我們必須先将對象的不可變部分提取出來,首先共享不變的部分。

2.提取享元中不可變的部分:

  對于文檔而言,字元普通存在;而對于Oozinoz公司而言,化學品到處都是。該公司的采購、工程、生産、安全等部門都在監控這成千上萬的化學品在整個工廠的流動情況。經過模組化,這些化學品都被描述為Substance類的執行個體。如下圖所示:

Flyweight(享元)模式

化學品被模組化為Substance類的執行個體

Substance類提供衆多可以通路其屬性的方法,另外還提供getMoles()方法用于傳回該化學品的摩爾數---即分子數量。Substance對象表示化學品的摩爾數。Oozinoz公司使用Mixture類來模拟化學品的組成。黑火藥由硝石粉、硫磺、碳粉等組成。這些都是Substance類的執行個體。

  考慮到Oozinoz公司的化學原料會越來越多,因而我們決定使用Flyweight模式來對這些化學品模組化,減少Substance對象的數量。為把Subsatnce對象模拟為享元,首先需要區分該類的可變部分和不可變部分。假設你決定重構Substance類,把其中不可變部分提出放入Chemical類中。

突破題:描述出被重構的Substance2類和新的不可變類,Chemical類:

Flyweight(享元)模式

該圖顯示Substance類的不可變屬性被提取出來放到單獨的類(Chemical)中

你可以把Subsance對象的不可變部分---包括化學制品的名稱、化學符号以及原子量(或分子量)---放入Chemical類中。如上圖所示。

  Substance2類保持對Chemical對象的引用。結果Subsance2類仍舊具有與早期版本Subsance類相同的屬性。從内部來講,這些附屬屬性依賴于Chemical類,就像Substance2類方法示範的那樣:

public double getAtomicWeight()
{
    return chemical.getAtomicWeight();
}

public double getGrams()
{
    return grams;
}

public douible getMoles()
{
    return grams/getAtomicWeight();
}
           

3. 共享享元:

   将對象的不可變部分提取出來僅是應用Flyweight模式前面的一半工作。後一半工作是建立一個享元工廠,用于執行個體化享元群組織共享享元的客戶代碼。另外,我們必須保證各個客戶代碼都将使用享元工廠建立享元執行個體,而不能自己建立。

   為了把Chemical對象變成享元,我們建立了一個用于生成化學品享元的工廠ChemicalFactory類,該類包含一個靜态方法,利用該方法可以傳回指定名稱的化學品享元的執行個體。在該工廠類初始化的時候,我們建立出各種已知常用的化學品享元執行個體,并将它們存放在一個散清單中。下圖給出了ChemicalFactory類的設計。

package com.oozinoz.chemical;
import java.util.*;

public class ChemicalFactory
{
    private static Map chemicals = new HashMap();

    static {
        chemicals.put("carbon",new Chemical("Carbon","C",12));
        chemicals.put("sulfur",new Chemical("Sulfur","S",32));
        chemicals.put("saltpeter",new Chemical("Saltpeter","KN03",101));
        //...
    }

    public static Chemical getChemical(String name)
    {
        return (Chemical) chemicals.get(name.toLowerCase());
    }
}
           
Flyweight(享元)模式

ChemicalFactory類是傳回Chemical對象的享元工廠類

  ChemicalFactory類的代碼使用靜态的初始化方法将Chemical對象存放在散清單中:

 在建立了享元化學品的工廠類之後,我們還必須采取某些措施來保證其他開發人員一定會用享元工廠類來建立享元,而不能直接執行個體化Chemical享元類。有一個簡單的做法就是借助Chemical類的可見性。

突破題:請問如何通過設定Chemical類的可見性來防止其他開發者直接執行個體化Chemical類?

答:一種方式是把Chemical類構造器定義為内部方法。這種做法可以防止ChemicalFactory類執行個體化Chemical類。

     為避免開發人員直接執行個體化Chemical類,可以将Chemical和ChemicalFactory放在同一個包中,并将Chemical類的構造器聲明為私有的。

  然而,僅通過設定可見性修飾符,還無法完全控制享元的執行個體化。如果要確定ChemicalFactory類是唯一可以建立Chemical類對象的類,那麼可以将Chemical類定義為ChemicalFactory的内部類(使用inner修飾符來定義)。

 為通路嵌套的類型,客戶代碼必須使用下面的表達式指定封裝類型。

ChemicalFactory.Chemical c = ChemicalFactory.getChemical("saltpeter");
           

 使用如下辦法可以簡化對被嵌套類的使用:将Chemical定義為接口,将嵌套類的名稱定義為ChemicalImpl。Chemical接口可以指定三個通路方法,如下所示:

package com.oozinoz.chemical2;
public interface Chemical
{
    String getName();
    String getSymbol();
    double getAtomicWeight();
}
           

 客戶代碼不直接引用内部類,是以可設定為私有,這樣就保證了隻有ChmicalFactory2類可通路它。

突破題:完成下面的ChemicalFactory2.java類的代碼:

    使用嵌套類雖然更加複雜,但是能夠徹底確定隻有ChemicalFactory2類可以執行個體化新的享元。 

package com.oozinoz.chemical2;
import java.util.*;

public class ChemicalFactory2
{
	private static Map chemicals = new HashMap();

	class ChemicalImpl implements Chemical
	{
		private String name;
		private String symbol;
		private double atomicWeight;

		ChemicalImpl(
				String name,
				String symbol,
				double atomicWeight){
			this.name = name;
			this.symbol = symbol;
			this.atomicWeight = atomicWeight;
		}

		public String getName()
		{
			return name;
		}

		public String getSymbol()
		{
			return symbol;
		}

		public double getAtomicWeight()
		{
			return atomicWeight;
		}

		public String toString()
		{
			return name+"("+symbol+")[" + atomicWeight + "]";
		}
	}

	static{
		ChemicalFactory2 factory = new ChemicalFactory2();
		chemicals.put("carbon",factory.new ChemicalImpl("Carbon","C",12));
		chemicals.put("sulfur",factory.new ChemicalImpl("Sulfur","S",32));
		chemicals.put("saltpeter",factory.new ChemicalImpl("Saltpeter","KN03",101));
		//...
	}

	public static Chemical getChemical(String name)
	{
		return (Chemical) chemicals.get(name.toLowerCase());
	}
}
           

上述代碼可解決三個問題:

1. ChemicalImpl嵌套類應該是私有的,保證隻有ChemicalFactory2類可以使用該類。請注意嵌套類的通路範圍必須是包範圍,或者可以公開通路,這樣包含的類可以執行個體化被嵌套的類。即使把構造器定義為公開通路的,如果嵌套類本身被辨別為私有,則沒有其他類可以使用這個構造器。

2.ChemicalFactory2構造器使用靜态執行個體化方法,以確定本類隻能建立一次化學藥品清單。

3.getChemical()方法應該在類的散清單中根據名稱來查找化學藥品。範例代碼使用小寫的藥品名稱來存儲和查找化學藥品。

4.小結:

  字元、化學品以及邊界等對象往往會大量出現,在對它們進行模組化的時候,可以使用Flyweight模式來管理對它們的共享通路。享元對象的屬性必須是不可變的。為了滿足這一特性,我們可以将這些對象中不可改變的部分提取出來建構成享元類。另外,為了確定享元對象能夠被共享,我們還必須提供一個用于建立和查找享元對象的工廠類,并且保證客戶隻能使用該類來建立享元對象。設定享元類的可見性修飾符可以在某些方面幫助我們控制其他開發者對享元對象的通路,但是内部類做得更好,能夠確定享元類隻能被享元工廠類通路。在確定客戶能夠适當地使用享元工廠之後,我們就可以出色地管理對大量細粒度對象的共享通路。

繼續閱讀