天天看點

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

概述

當一個軟體系統在運作時産生的對象數量太多,将導緻運作代價過高,帶來系統性能下降等問題。例如在一個文本字元串中存在很多重複的字元,如果每一個字元都用一個單獨的對象來表示,将會占用較多的記憶體空間,那麼我們如何去避免系統中出現大量相同或相似的對象,同時又不影響用戶端程式通過面向對象的方式對這些對象進行操作?享元模式正為解決這一類問題而誕生。享元模式通過共享技術實作相同或相似對象的重用,在邏輯上每一個出現的字元都有一個對象與之對應,然而在實體上它們卻共享同一個享元對象,這個對象可以出現在一個字元串的不同地方,相同的字元對象都指向同一個執行個體,在享元模式中,存儲這些共享執行個體對象的地方稱為享元池(Flyweight Pool)。我們可以針對每一個不同的字元建立一個享元對象,将其放在享元池中,需要時再從享元池取出。

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

享元模式以共享的方式高效地支援大量細粒度對象的重用,享元對象能做到共享的關鍵是區分了内部狀态(Intrinsic State)和外部狀态(Extrinsic State)。

(1)  内部狀态是存儲在享元對象内部并且不會随環境改變而改變的狀态,内部狀态可以共享。如字元的内容,不會随外部環境的變化而變化,無論在任何環境下字元“a”始終是“a”,都不會變成“b”。

(2)  外部狀态是随環境改變而改變的、不可以共享的狀态。享元對象的外部狀态通常由用戶端儲存,并在享元對象被建立之後,需要使用的時候再傳入到享元對象内部。一個外部狀态與另一個外部狀态之間是互相獨立的。如字元的顔色,可以在不同的地方有不同的顔色,例如有的“a”是紅色的,有的“a”是綠色的,字元的大小也是如此,有的“a”是五号字,有的“a”是四号字。而且字元的顔色和大小是兩個獨立的外部狀态,它們可以獨立變化,互相之間沒有影響,用戶端可以在使用時将外部狀态注入享元對象中。

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

享元模式

運用共享技術有效地支援大量細粒度對象的複用。系統隻使用少量的對象,而這些對象都很相似,狀态變化很小,可以實作對象的多次複用。由于享元模式要求能夠共享的對象必須是細粒度對象,是以它又稱為輕量級模式,它是一種對象結構型模式。

結構

享元模式結構較為複雜,一般結合工廠模式一起使用,在它的結構圖中包含了一個享元工廠類。

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

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

● Flyweight(抽象享元類):通常是一個接口或抽象類,在抽象享元類中聲明了具體享元類公共的方法,這些方法可以向外界提供享元對象的内部資料(内部狀态),同時也可以通過這些方法來設定外部資料(外部狀态)。

● ConcreteFlyweight(具體享元類):它實作了抽象享元類,其執行個體稱為享元對象;在具體享元類中為内部狀态提供了存儲空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元對象。

● UnsharedConcreteFlyweight(非共享具體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的對象時可以直接通過執行個體化建立。

● FlyweightFactory(享元工廠類):享元工廠類用于建立并管理享元對象,它針對抽象享元類程式設計,将各種類型的具體享元對象存儲在一個享元池中,享元池一般設計為一個存儲“鍵值對”的集合(也可以是其他類型的集合),可以結合工廠模式進行設計;當使用者請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已建立的執行個體或者建立一個新的執行個體(如果不存在的話),傳回新建立的執行個體并将其存儲在享元池中。

class FlyweightFactory {
    //定義一個HashMap用于存儲享元對象,實作享元池
       private HashMap flyweights = new HashMap();
      
       public Flyweight getFlyweight(String key){
              //如果對象存在,則直接從享元池擷取
              if(flyweights.containsKey(key)){
                     return (Flyweight)flyweights.get(key);
              }
              //如果對象不存在,先建立一個新的對象添加到享元池中,然後傳回
              else {
                     Flyweight fw = new ConcreteFlyweight();
                     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) {
              ......
       }     
}
           

Demo

一個咖啡店有幾種口味的咖啡(拿鐵、摩卡、卡布奇諾等等),如果這家店接到分訂單要幾十杯咖啡。那麼顯然咖啡的口味就可以設定成共享的,而不必為每一杯單獨生成。代碼實作如下:

public abstract class Order {
	// 執行賣出動作
	public abstract void sell();
}
           
public class FlavorOrder extends Order {
	public String flavor;
	// 擷取咖啡口味
	public FlavorOrder(String flavor) {
	   this.flavor = flavor;
	}
	@Override
	public void sell() {
	   // TODO Auto-generated method stub
	   System.out.println("賣出一份" + flavor + "的咖啡。");
	}
}
           
public class FlavorFactory {
	private Map<String, Order> flavorPool = new HashMap<String, Order>();
// 靜态工廠,負責生成訂單對象
	private static FlavorFactory flavorFactory = new FlavorFactory();
	private FlavorFactory() {}
	public static FlavorFactory getInstance() {
	   return flavorFactory;
	}
	public Order getOrder(String flavor) {
	   Order order = null;
	   if (flavorPool.containsKey(flavor)) {// 如果此映射包含指定鍵的映射關系,則傳回 true
		    order = flavorPool.get(flavor);
	   } else {
		    order = new FlavorOrder(flavor);
		    flavorPool.put(flavor, order);
	   }
	   return order;
	}
	public int getTotalFlavorsMade() {
   		return flavorPool.size();
	}
}
           
public class Client {
	// 客戶下的訂單
	private static List<Order> orders = new ArrayList<Order>();
	// 訂單對象生成工廠
	private static FlavorFactory flavorFactory;
	// 增加訂單
	private static void takeOrders(String flavor) {
	   orders.add(flavorFactory.getOrder(flavor));
	}
	public static void main(String[] args) {
  		// 訂單生成工廠
	   flavorFactory = FlavorFactory.getInstance();
	   // 增加訂單
	   takeOrders("摩卡");
	   takeOrders("卡布奇諾");
	   takeOrders("香草星冰樂");
	   takeOrders("香草星冰樂");
 	   takeOrders("拿鐵");
	   takeOrders("卡布奇諾");
	   takeOrders("拿鐵");
	   takeOrders("卡布奇諾");
	   takeOrders("摩卡");
	   takeOrders("香草星冰樂");
	   takeOrders("卡布奇諾");
	   takeOrders("摩卡");
	   takeOrders("香草星冰樂");
	   takeOrders("拿鐵");
	   takeOrders("拿鐵");
	   // 賣咖啡
	   for (Order order : orders) {
		    order.sell();
	   }
	   // 列印生成的訂單java對象數量
	   System.out.println("\n客戶一共買了 " + orders.size() + " 杯咖啡! ");
	   // 列印生成的訂單java對象數量
	   System.out.println("共生成了 " + flavorFactory.getTotalFlavorsMade()
     + " 個 FlavorOrder java對象! ");
	}
}
           

賣出一份摩卡的咖啡。

賣出一份卡布奇諾的咖啡。

賣出一份香草星冰樂的咖啡。

賣出一份拿鐵的咖啡。

客戶一共買了 15 杯咖啡!

共生成了 4 個 FlavorOrder java對象!

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

  • 單純享元模式
設計模式(十五)享元模式(結構型)
  • 複合享元模式

将一些單純享元對象使用組合模式加以組合,還可以形成複合享元對象,這樣的複合享元對象本身不能共享,但是它們可以分解成單純享元對象,而後者則可以共享。

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

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

與其他模式的聯用

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

(1)在享元模式的享元工廠類中通常提供一個靜态的工廠方法用于傳回享元對象,使用簡單工廠模式來生成享元對象。

(2)在一個系統中,通常隻有唯一一個享元工廠,是以可以使用單例模式進行享元工廠類的設計。

(3)享元模式可以結合組合模式形成複合享元模式,統一對多個享元對象設定外部狀态。

享元模式與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

false

可以看出,前兩個輸出語句均為true,說明str1、str2、str3在記憶體中引用了相同的對象;如果有一個字元串str4,其初值為"ab",再對它進行操作str4 += "cd",此時雖然str4的内容與str1相同,但是由于str4的初始值不同,在建立str4時重新配置設定了記憶體,是以第三個輸出語句結果為false;最後一個輸出語句結果也為false,說明當對str2進行修改時将建立一個新的對象,修改工作在新對象上完成,而原來引用的對象并沒有發生任何改變,str1仍然引用原有對象,而str2引用新對象,str1與str2引用了兩個完全不同的對象。

總結

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

1.主要優點

享元模式的主要優點如下:

(1) 可以極大減少記憶體中對象的數量,使得相同或相似對象在記憶體中隻儲存一份,進而可以節約系統資源,提高系統性能。

(2) 享元模式的外部狀态相對獨立,而且不會影響其内部狀态,進而使得享元對象可以在不同的環境中被共享。

2.主要缺點

享元模式的主要缺點如下:

(1) 享元模式使得系統變得複雜,需要分離出内部狀态和外部狀态,這使得程式的邏輯複雜化。

(2) 為了使對象可以共享,享元模式需要将享元對象的部分狀态外部化,而讀取外部狀态将使得運作時間變長。

3.适用場景

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

(1) 一個系統有大量相同或者相似的對象,造成記憶體的大量耗費。

(2) 對象的大部分狀态都可以外部化,可以将這些外部狀态傳入對象中。

(3) 在使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,是以,應當在需要多次重複使用享元對象時才值得使用享元模式。

我是天王蓋地虎的分割線

作者:我愛物聯網

出處:http://yydcdut.cnblogs.com/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。