設計模式-享元模式(Flyweight)-Java

設計模式-享元模式(Flyweight)-Java


目錄




內容

  當前咱們國家正在大力倡導構建和諧社會,其中一個很重要的組成部分就是建設資源節約型社會,“浪費可恥,節儉光榮”。在軟件系統中,有時候也會存在資源浪費的情況,例如在計算機內存中存儲了多個完全相同或者非常相似的對象,如果這些對象的數量太多將導致系統運行代價過高,內存屬於計算機的“稀缺資源”,不應該用來“隨便浪費”,那麼是否存在一種技術可以用於節約內存使用空間,實現對這些相同或者相似對象的共享訪問呢?答案是肯定,這種技術就是我們本章將要學習的享元模式。

1、示例案例-圍棋棋子的設計

  Sunny軟件公司欲開發一個圍棋軟件,其界面效果如圖1-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZhFXlCCl-1591831846184)(./images/圍棋.png)]
圖1-1 圍棋軟件界面效果圖

  Sunny軟件公司開發人員通過對圍棋軟件進行分析,發現在圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作爲一個獨立的對象存儲在內存中,將導致該圍棋軟件在運行時所需內存空間較大,如何降低運行代價、提高系統性能是Sunny公司開發人員需要解決的一個問題。爲了解決這個問題,Sunny公司開發人員決定使用享元模式來設計該圍棋軟件的棋子對象,那麼享元模式是如何實現節約內存進而提高系統性能的呢?彆着急,下面讓我們正式進入享元模式的學習。

2、享元模式概述

  當一個軟件系統在運行時產生的對象數量太多,將導致運行代價過高,帶來系統性能下降等問題。例如在一個文本字符串中存在很多重複的字符,如果每一個字符都用一個單獨的對象來表示,將會佔用較多的內存空間,那麼我們如何去避免系統中出現大量相同或相似的對象,同時又不影響客戶端程序通過面向對象的方式對這些對象進行操作?享元模式正爲解決這一類問題而誕生。享元模式通過共享技術實現相同或相似對象的重用,在邏輯上每一個出現的字符都有一個對象與之對應,然而在物理上它們卻共享同一個享元對象,這個對象可以出現在一個字符串的不同地方,相同的字符對象都指向同一個實例,在享元模式中,存儲這些共享實例對象的地方稱爲享元池(Flyweight Pool)。我們可以針對每一個不同的字符創建一個享元對象,將其放在享元池中,需要時再從享元池取出。如圖2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8juGVRsd-1591831846186)(./images/flyweightPool.png)]
圖2-1 字符享元對象示意圖

 emsp;享元模式以共享的方式高效地支持大量的細粒度對象的重用,享元對象能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。下面將對享元的內部狀態和外部狀態進行簡單的介紹:

  • (1):內部狀態是存儲在享元對象內部並且不會隨環境改變而改變的狀態,內部狀態可以共享。如字符的內容,不會隨外部環境的變化而變化,無論在任何環境下字符"a"始終是"a",都不會變成“b"。
  • (2):外部狀態是隨環境改變而改變的、不可以共享的狀態。享元對象的外部狀態通常由客戶端保存,並在享元對象被創建之後,需要使用的時候再傳入到享元對象內部。一個外部狀態與另一個外部狀態之間是相互獨立的。如字符的顏色,可以在不同的地方由不同的顏色,例如由的“a"是紅色的,由的"a"是綠色的,字符的大小也是如此,由的”a"是五號字,由的"a"是四號字。而且字符的顏色和大小是兩個獨立的外部狀態,它們可以獨立變化,相互之間沒有影響,客戶端可以在使用時將外部狀態注入享元對象中。

  正因爲區分了內部狀態和外部狀態,我們可以將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的,需要的時候就將對象從享元池中取出,實現對象的複用。通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份。

2.1、享元模式定義

  • 享元模式(FlyWeight Pattern):運用共享技術有效地支持大量細粒度對象的複用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次複用。由於享元模式要求能夠共享的對象必須是細粒度對象,因此它又稱爲輕量級模式,它是一種對象結構型模式。

2.2、享元模式結構

  享元模式結構較爲複雜,一般結合工廠模式一起使用,在它的結構圖中包含了一個享元工廠類,其結構圖如圖2.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BQ6j2Had-1591831846187)(./images/model_flyweight.png)]
圖2.2-1 享元模式結構圖

2.3、享元模式結構圖中角色

  在享元模式結構圖中包含如下幾個角色:

  • Flyweight(抽象享元類):通常是一個接口或抽象類,在抽象享元類中聲明瞭具體享元類公共的方法,這些方法可以向外界提供享元對象的內部數據(內部狀態),同時也可以通過這些方法來設置外部數據(外部狀態)。
  • ConcreteFlyweight(具體享元類):它實現了抽象享元類,其實例稱爲享元對象;在具體享元類中爲內部狀態提供了存儲空間。通常我們可以結合單例模式來設計具體享元類,爲每一個具體享元類提供唯一的享元對象。
  • UnsharedConcreteFlyweight(非共享具體享元類):並不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計爲非共享具體享元類;當需要一個非共享具體享元類的對象時可以之間通過實例化創建。
  • FlyweightFactory(享元工廠類):享元工廠類用於創建並管理享元對象,它針對抽象享元類編程,將各種類型的具體享元對象存儲在一個享元池中,享元池一般設計爲一個存儲“鍵值對”的集合(也可以是其他類型的集合),可以結合工廠模式進行設計;當用戶請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已創建的實例或者創建一個新的實例(如果不存在的話),返回新創建的實例並將其存儲在享元池中。

2.4、享元模式典型實現

  在享元模式中引入了享元工廠類,享元工廠類的作用在於提供一個用於存儲享元對象的享元池,當用戶需要對象時,首先從享元池中獲取,如果享元池中不存在,則創建一個新的享元對象返回給用戶,並在享元池中保存該新增對象。典型的享元工廠類的代碼如下:

class FlyweightFactory {
//定義一個HashMap用於存儲享元對象,實現享元池
private HashMap<Flyweight> flyweights = new HashMap<>();

public Flyweight getFlyweight(String key){
//如果對象存在,則直接從享元池獲取
if(flyweights.containsKey(key)){
return(Flyweight)flyweights.get(key);
}
//如果對象不存在,先創建一個新的對象添加到享元池中,然後返回
else {
Flyweight fw = newConcreteFlyweight();
flyweights.put(key,fw);
return fw;
}
}
}

  享元類的設計是享元模式的要點之一,在享元類中要將內部狀態和外部狀態分開處理,通常將內部狀態作爲享元類的成員變量,而外部狀態通過注入的方式添加到享元類中。典型的享元類代碼如下所示:

class Flyweight {
//內部狀態intrinsicState作爲成員變量,同一個享元對象其內部狀態是一致的
private String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState=intrinsicState;
}
//外部狀態extrinsicState在使用時由外部設置,不保存在享元對象中,即使是同一個對象,在每一次調用時也可以傳入不同的外部狀態
public void operation(String extrinsicState) {
......
}
}

3、圍棋棋子-享元模式完整解決方案

  爲了節約存儲空間,提高系統性能,Sunny公司開發人員使用享元模式來設計圍棋軟件中的棋子,其基本結構如圖3-1所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Fayl9Ise-1591831846189)(./images/igoChessman.png)]
圖3-1圍棋棋子結構圖

  在圖3-1IoChessman充當抽象享元類,BlackIgoChessman和WhiteIgoChessman充當具體享元類,IgoChessmanFactory充當享元工廠類。完整代碼如下所示:

  • IgoChessman類代碼3-1:圍棋棋子類-抽象享元類

      package flyweight;
    
      //圍棋棋子類:抽象享元類
      public abstract class IgoChessman {
      	public abstract String getColor();
    
      	public void display() {
      		System.out.println("棋子顏色:" + this.getColor());
      	}
      }
    
  • BlackIgoChessman類代碼3-2:黑色棋子類-具體享元類

      package flyweight;
    
      //黑色棋子類:具體享元類
      public class BlackIgoChessman extends IgoChessman{
    
      	@Override
      	public String getColor() {
      		return "黑色";
      	}
      }
    
  • WhiteIgoChessman類代碼3-3:白色棋子類-具體享元類

      package flyweight;
    
      //白色棋子類:具體享元類
      public class WhiteIgoChessman extends IgoChessman{
      	@Override
      	public String getColor() {
      		return "白色";
      	}
      }
    
  • IgoChessmanFactory類代碼3-4:圍棋棋子工廠類-享元工廠類,當了模式設計

      package flyweight;
    
      import java.util.HashMap;
    
      //圍棋棋子工廠類:享元工廠類,使用單例模式進行設計
      public class IgoChessmanFactory {
      	private static IgoChessmanFactory instance = new IgoChessmanFactory();
      	private static HashMap<String, IgoChessman> hm;
    
      	private IgoChessmanFactory() {
      		hm = new HashMap<>();
      		hm.put("b", new BlackIgoChessman());
      		hm.put("w", new WhiteIgoChessman());
      	}
    
      	public static IgoChessmanFactory getInstance() {
      		return instance;
      	}
    
      	public  IgoChessman getIgoChessman(String key) {
      		return hm.get(key);
      	}
      }
    
  • Client類代碼3-5:客戶端測試類

      package flyweight;
    
      public class Client {
      	public static void main(String[] args) {
      		IgoChessman b1, b2, b3, w1, w2;
      		IgoChessmanFactory factory;
    
      		factory = IgoChessmanFactory.getInstance();
    
      		b1 = factory.getIgoChessman("b");
      		b2 = factory.getIgoChessman("b");
      		b3 = factory.getIgoChessman("b");
      		System.out.println("判斷兩顆黑子是否相同:" + (b1 == b2));
      		w1 = factory.getIgoChessman("w");
      		w2 = factory.getIgoChessman("w");
      		System.out.println("判斷兩顆白子是否相同:" + (w1==w2));
    
      		b1.display();
      		b2.display();
      		b3.display();
      		w1.display();
      		w2.display();
      	}
      }
    
  • 測試結果:

      判斷兩顆黑子是否相同:true
      判斷兩顆白子是否相同:true
      棋子顏色:黑色
      棋子顏色:黑色
      棋子顏色:黑色
      棋子顏色:白色
      棋子顏色:白色
    

  從輸出結果可以看出,雖然我們獲取了三個黑子對象和兩個白子對象,但是它們的內存地址相同,也就是說,它們實際上是同一個對象。在實現享元工廠類時我們使用了單例模式和簡單工廠模式,確保了享元工廠對象的唯一性,並提供工廠方法來向客戶端返回享元對象。

4、帶外部狀態的解決方案

  Sunny軟件公司開發人員通過對圍棋棋子進行進一步分析,發現雖然黑色棋子和白色棋子可以共享,但是它們將顯示在棋盤的不同位置,如何讓相同的黑子或者白子能夠多次重複顯示且位於一個棋盤的不同地方?解決方法就是將棋子的位置定義爲棋子的一個外部狀態,在需要時再進行設置。因此,我們在圖3-1中增加了一個新的類Coordinates(座標類),用於存儲每一個棋子的位置,修改之後的結構圖如圖4-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lVl7FiBW-1591831846191)(./images/igoChessman_coordinate.png)]
圖4-1 引入外部狀態之後的圍棋棋子結構圖

在圖4-1中,除了增加一個座標類Coordinates以外,抽象享元類IgoChessman中的display()方法也將對應增加一個Coordinates類型的參數,用於在顯示棋子時指定其座標,Coordinates類和修改之後的IgoChessman類的代碼如下所示:

package flyweight;

//座標類:外部狀態類
public class Coordinate {
	private int x;
	private int y;

	public Coordinate() {}

	public Coordinate(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	@Override
	public String toString() {
		return "(" + this.x + ", " + this.y + ")";
	}
}

//圍棋棋子類:抽象享元類
package flyweight;

//圍棋棋子類:抽象享元類
public abstract class IgoChessman {
	public abstract String getColor();

	public void display(Coordinate coord) {
		System.out.println("棋子顏色:" + this.getColor() + "," + coord);
	}
}

客戶端測試代碼修改如下:
package flyweight;

public class Client {
	public static void main(String[] args) {
		IgoChessman b1, b2, b3, w1, w2;
		IgoChessmanFactory factory;

		factory = IgoChessmanFactory.getInstance();

		b1 = factory.getIgoChessman("b");
		b2 = factory.getIgoChessman("b");
		b3 = factory.getIgoChessman("b");
		System.out.println("判斷兩顆黑子是否相同:" + (b1 == b2));
		w1 = factory.getIgoChessman("w");
		w2 = factory.getIgoChessman("w");
		System.out.println("判斷兩顆白子是否相同:" + (w1==w2));

		b1.display(new Coordinate(1, 2));
		b2.display(new Coordinate(3, 4));
		b3.display(new Coordinate(1, 3));
		w1.display(new Coordinate(2, 5));
		w2.display(new Coordinate(2, 4));
	}
}
測試結果:
判斷兩顆黑子是否相同:true
判斷兩顆白子是否相同:true
棋子顏色:黑色,(1, 2)
棋子顏色:黑色,(3, 4)
棋子顏色:黑色,(1, 3)
棋子顏色:白色,(2, 5)
棋子顏色:白色,(2, 4)

  從輸出結果可以看到,在每次調用display()方法時,都設置了不同的外部狀態——座標值,因此相同的棋子對象雖然具有相同的顏色,但是它們的座標值不同,將顯示在棋盤的不同位置。

5、單純享元模式和複合享元模式

  標準的享元模式結構圖中既包含可以共享的具體享元類,也包含不可以共享的非共享具體享元類。但是在實際使用過程中,我們有時候會用到兩種特殊的享元模式:單純享元模式和複合享元模式,下面將對這兩種特殊的享元模式進行簡單的介紹:

5.1、單純享元模式

  在單純享元模式中,所有的具體享元類都是可以共享的,不存在非共享具體享元類。單純享元模式的結構如圖5.1-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K50D4Ndo-1591831846191)(./images/flyweight_dancun.png)]
圖5.1-1 單純享元模式結構圖

5.2、複合享元模式

  將一些單純享元對象使用組合模式加以組合,還可以形成複合享元對象,這樣的複合享元對象本身不能共享,但是它們可以分解成單純享元對象,而後者則可以共享。複合享元模式的結構如圖5.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1jlfZCkL-1591831846193)(./images/flyweight_fuhe.png)]
圖5.2-1 符合享元模式結構圖

  通過複合享元模式,可以確保複合享元類CompositeConcreteFlyweight中所包含的每個單純享元類ConcreteFlyweight都具有相同的外部狀態,而這些單純享元的內部狀態往往可以不同。如果希望爲多個內部狀態不同的享元對象設置相同的外部狀態,可以考慮使用複合享元模式。

6、補充

6.1、與其他模式的聯用

  享元模式通常需要和其他模式一起聯用,幾種常見的聯用方式如下:

  • (1))在享元模式的享元工廠類中通常提供一個靜態的工廠方法用於返回享元對象,使用簡單工廠模式來生成享元對象。
  • (2))在一個系統中,通常只有唯一一個享元工廠,因此可以使用單例模式進行享元工廠類的設計。
  • (3)享元模式可以結合組合模式形成複合享元模式,統一對多個享元對象設置外部狀態。

6.2、享元模式與String 類

  JDK類庫中的String類使用了享元模式,我們通過如下代碼來加以說明:

class Demo {
public static void main(String args[]) {
String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = "ab";
str4 += "cd";
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1 == str4);
str2 += "e";
System.out.println(str1 == str2);
}
}

  在Java語言中,如果每次執行類似String str1=“abcd"的操作時都創建一個新的字符串對象將導致內存開銷很大,因此如果第一次創建了內容爲"abcd"的字符串對象str1,下一次再創建內容相同的字符串對象str2時會將它的引用指向"abcd”,不會重新分配內存空間,從而實現了"abcd"在內存中的共享。上述代碼輸出結果如下:

true
true
false
false

  可以看出,前兩個輸出語句均爲true,說明str1、str2、str3在內存中引用了相同的對象;如果有一個字符串str4,其初值爲"ab",再對它進行操作str4 += “cd”,此時雖然str4的內容與str1相同,但是由於str4的初始值不同,在創建str4時重新分配了內存,所以第三個輸出語句結果爲false;最後一個輸出語句結果也爲false,說明當對str2進行修改時將創建一個新的對象,修改工作在新對象上完成,而原來引用的對象並沒有發生任何改變,str1仍然引用原有對象,而str2引用新對象,str1與str2引用了兩個完全不同的對象。

6.3、擴展

  關於Java String類這種在修改享元對象時,先將原有對象複製一份,然後在新對象上再實施修改操作的機制稱爲“Copy On Write”,大家可以自行查詢相關資料來進一步瞭解和學習“Copy On Write”機制,在此不作詳細說明。

7、總結

  當系統中存在大量相同或者相似的對象時,享元模式是一種較好的解決方案,它通過共享技術實現相同或相似的細粒度對象的複用,從而節約了內存空間,提高了系統性能。相比其他結構型設計模式,享元模式的使用頻率並不算太高,但是作爲一種以“節約內存,提高性能”爲出發點的設計模式,它在軟件開發中還是得到了一定程度的應用。

7.1、優缺點

  • 主要優點
    • (1)可以極大的減少內存中對象的數量,使得相同或相似對象在內存中只保存一份,從而可以節約系統資源,提高系統性能。
    • (2)享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元對象可以在不同的環境中被共享。
  • 主要缺點
    • (1)享元模式使得系統變得複雜,需要分離出內部狀態和外部狀態,這使得程序的邏輯複雜化。
    • (2)爲了使對象可以共享,享元模式需要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間邊長。

7.2、適用場景

  在以下情況下可以考慮使用享元模式:

  • (1) 一個系統有量相同或者相似的對象,造成內存的大量耗費。
  • (2) 對象的大部分狀態都可以外部化,可以將這些外部狀態傳入對象中。
  • (3) 在使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,因此,應當在需要多次重複使用享元對象時才值得使用享元模式。

後記

  參考文獻:Java設計模式(劉偉).pdf。持續更新,歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章