目錄
本文的結構如下:
- 引言
- 什么是享元模式
- 模式的結構
- 典型代碼
- 代碼示例
- 單純享元模式和復合享元模式
- 模式擴展
- 優點和缺點
- 適用環境
- 模式應用
一、引言
衣服小了,沒有辦法只能買新的,衣服破了一個小口,無傷大雅,則可以穿針引線縫補妥當。如果是黑色的衣服,選上黑色的細線是合適的,灰色的衣服配上灰色的細線是適宜的,白色的衣服搭上白色的細線也是恰好的......至于針,一直就是那根針。
在軟件開發中,也有差不多的情況,比如一款電子圍棋游戲,有很多白子黑子,除了顏色和位置不同外,棋子其它都是一樣的,為每個棋子實例化一個對象顯然是一種浪費,像“針線活”中的“針”一樣被共享,才是教科學的做法,畢竟“勤儉持家”是我們的美德。當存在多個相同對象的時候,可以通過共享對象進而減少相同對象創建引起的內存消耗,提高程序性能。這就是設計模式中的享元模式。
二、什么是享元模式
“享”是共享的意思,“元”指的是元件,也就是小顆粒的東西,享元顧名思義便是共享小部件,很多系統或者程序包含大量對象,但是這些對象絕大多數都是差不多的,除了一些極個別的屬性外。當一個軟件系統在運行時產生的對象數量太多,將導致運行代價過高,帶來系統性能下降等問題。享元模式正為解決這一類問題而誕生。。
享元模式以共享的方式高效地支持大量細粒度對象的重用,在享元模式中,存儲這些共享實例對象的地方稱為享元池(Flyweight Pool)。享元對象能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。
- 內部狀態是存儲在享元對象內部并且不會隨環境改變而改變的狀態,內部狀態可以共享。就像“針線活”中的針。
- 外部狀態是隨環境改變而改變的、不可以共享的狀態。享元對象的外部狀態通常由客戶端保存,并在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。一個外部狀態與另一個外部狀態之間是相互獨立的。就像“針線活”中的線。
正因為區分了內部狀態和外部狀態,可以將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的,需要的時候就將對象從享元池中取出,實現對象的復用。通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份。
享元模式定義如下:
享元模式(Flyweight Pattern):運用共享技術有效地支持大量細粒度對象的復用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次復用。由于享元模式要求能夠共享的對象必須是細粒度對象,因此它又稱為輕量級模式,它是一種對象結構型模式。
三、模式的結構
享元模式一般結合工廠模式一起使用,在它的結構圖中包含了一個享元工廠類,UML類圖如下:
在享元模式結構圖中包含如下幾個角色:
- Flyweight(抽象享元類):通常是一個接口或抽象類,在抽象享元類中聲明了具體享元類公共的方法,這些方法可以向外界提供享元對象的內部數據(內部狀態),同時也可以通過這些方法來設置外部數據(外部狀態)。
- ConcreteFlyweight(具體享元類):它實現了抽象享元類,其實例稱為享元對象;在具體享元類中為內部狀態提供了存儲空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元對象。
- UnsharedConcreteFlyweight(非共享具體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的對象時可以直接通過實例化創建。
- FlyweightFactory(享元工廠類):享元工廠類用于創建并管理享元對象,它針對抽象享元類編程,將各種類型的具體享元對象存儲在一個享元池中,享元池一般設計為一個存儲“鍵值對”的集合(也可以是其他類型的集合),可以結合工廠模式進行設計;當用戶請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已創建的實例或者創建一個新的實例(如果不存在的話),返回新創建的實例并將其存儲在享元池中。
四、典型代碼
在享元模式中引入了享元工廠類,享元工廠類的作用在于提供一個用于存儲享元對象的享元池,當用戶需要對象時,首先從享元池中獲取,如果享元池中不存在,則創建一個新的享元對象返回給用戶,并在享元池中保存該新增對象。
public class FlyweightFactory {
//定義一個HashMap用于存儲享元對象,實現享元池
private static final Map<String, Flyweight> FLYWEIGHTS = new ConcurrentHashMap();
private static final FlyweightFactory INSTANCE = new FlyweightFactory();
private FlyweightFactory(){}
public static FlyweightFactory getInstance(){
return INSTANCE;
}
public Flyweight getFlyweight(String key){
//如果對象存在,則直接從享元池獲取
if (FLYWEIGHTS.containsKey(key)){
return FLYWEIGHTS.get(key);
}else {
//如果對象不存在,先創建一個新的對象添加到享元池中,然后返回
Flyweight flyweight = new ConcreteFlyweight("intrinsicState");
FLYWEIGHTS.put(key, flyweight);
return flyweight;
}
}
}
享元類的設計是享元模式的要點之一,在享元類中要將內部狀態和外部狀態分開處理,通常將內部狀態作為享元類的成員變量,而外部狀態通過注入的方式添加到享元類中。典型代碼如下:
public interface Flyweight {
String getIntrinsicState();
void operation(String extrinsicState);
}
public class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState){
this.intrinsicState = intrinsicState;
}
public String getIntrinsicState() {
return intrinsicState;
}
public void operation(String extrinsicState) {
System.out.println(extrinsicState);
}
}
五、代碼示例
這里以撲克牌為例,假設沒有大小王,一副撲克牌有52張。
5.1、不用享元模式
public class Card {
private String color;//花色
private String num;//點數
public Card(String color, String num){
this.color = color;
this.num = num;
}
public String getColor() {
return color;
}
public String getNum() {
return num;
}
public void setColor(String color) {
this.color = color;
}
public void setNum(String num) {
this.num = num;
}
public String toString(){
return "Card[牌色=" + color + ",牌數=" + num + "]";
}
}
隨機分配4張:
public class Game {
public static void main(String[] args) {
String[] colors = {"黑桃", "草花", "紅桃", "方塊"};
List<Card> cards = new LinkedList<Card>();
for (int i=0; i<4; i++){
for (int j=1; j<=13; j++){
switch (j){
case 11:
cards.add(new Card(colors[i], "J"));
break;
case 12:
cards.add(new Card(colors[i], "Q"));
break;
case 13:
cards.add(new Card(colors[i], "K"));
break;
default:
cards.add(new Card(colors[i], String.valueOf(j)));
break;
}
}
}
System.out.println("隨機分四張牌:");
for (int i=0; i<4; i++){
System.out.println(cards.get((int) (Math.random()*52)));
}
}
}
結果是:
隨機分四張牌:
Card[牌色=方塊,牌數=3]
Card[牌色=紅桃,牌數=K]
Card[牌色=紅桃,牌數=J]
Card[牌色=紅桃,牌數=7]
顯然這里要創建52個Card對象,但這里花色只有四種四固定的,不同的是大小,可以用享元模式來共享對象,減少內存消耗。
5.2、使用享元模式
抽象卡牌類:
public abstract class Card {
public abstract void showCards(String num);//享元類外部狀態通過參數傳入
}
具體卡牌類:
public class SpadeCard extends Card {
private String color = "黑桃";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌數=" + num + "]");
}
}
public class ClubCard extends Card {
private String color = "草花";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌數=" + num + "]");
}
}
public class HeartsCard extends Card {
private String color = "紅桃";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌數=" + num + "]");
}
}
public class DiamondsCard extends Card {
private String color = "方塊";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌數=" + num + "]");
}
}
享元工廠:
public class CardFactory {
public static final int SPADE = 1;
public static final int CLUB = 2;
public static final int HEARTS = 3;
public static final int DIAMONDS = 4;
private static Map<Integer, Card> cards = new ConcurrentHashMap<Integer, Card>();
private static CardFactory instance = new CardFactory();
private CardFactory(){}
public static CardFactory getInstance(){
return instance;
}
public Card getCard(Integer color){
if (cards.containsKey(color)){
System.out.println("復用對象");
return cards.get(color);
}else {
System.out.println("新建對象");
Card card;
switch (color){
case SPADE: card = new SpadeCard();break;
case CLUB: card = new ClubCard();break;
case HEARTS:card = new HeartsCard();break;
default:card = new DiamondsCard();break;
}
cards.put(color, card);
return card;
}
}
}
測試隨機發10張:
public class Game {
public static void main(String[] args) {
CardFactory factory = CardFactory.getInstance();
for (int i=0; i<10; i++ ){
Card card = null;
//隨機花色
switch ((int)(Math.random()*4)){
case 0: card = factory.getCard(CardFactory.SPADE);break;
case 1: card = factory.getCard(CardFactory.CLUB);break;
case 2: card = factory.getCard(CardFactory.HEARTS);break;
case 3: card = factory.getCard(CardFactory.DIAMONDS);break;
}
//隨機大小
if (card != null){
int num = (int)(Math.random()*13) + 1;
switch (num){
case 11: card.showCards("J");break;
case 12: card.showCards("Q");break;
case 13: card.showCards("K");break;
default: card.showCards(String.valueOf(num));break;
}
}
}
}
}
結果是:
新建對象
Card[牌色=方塊,牌數=2]
新建對象
Card[牌色=紅桃,牌數=6]
復用對象
Card[牌色=紅桃,牌數=6]
新建對象
Card[牌色=草花,牌數=3]
復用對象
Card[牌色=方塊,牌數=K]
新建對象
Card[牌色=黑桃,牌數=7]
復用對象
Card[牌色=黑桃,牌數=2]
復用對象
Card[牌色=黑桃,牌數=6]
復用對象
Card[牌色=方塊,牌數=J]
復用對象
Card[牌色=紅桃,牌數=7]
這里有拿到相同的花色和大小,因為這里的random并沒有去重復,不是很嚴謹,只是為了舉例說明。
六、單純享元模式和復合享元模式
標準的享元模式結構圖中既包含可以共享的具體享元類,也包含不可以共享的非共享具體享元類(不共享的一半直接實例化即可)。但是在實際使用過程中,我們有時候會用到兩種特殊的享元模式:單純享元模式和復合享元模式。
6.1、單純享元模式
在單純享元模式中,所有的具體享元類都是可以共享的,不存在非共享具體享元類。它的UML類圖如下:
6.2、復合享元模式
在單純享元模式中,所有的享元對象都是單純享元對象,也就是說都是可以直接共享的。而還有一種較為復雜的情況,將一些單純享元使用合成模式加以復合,形成復合享元對象。這樣的復合享元對象本身不能共享,但是它們可以分解成單純享元對象,而后者則可以共享。它的UML類圖如下:
通過復合享元模式,可以確保復合享元類CompositeConcreteFlyweight中所包含的每個單純享元類ConcreteFlyweight都具有相同的外部狀態,而這些單純享元的內部狀態往往可以不同。如果希望為多個內部狀態不同的享元對象設置相同的外部狀態,可以考慮使用復合享元模式。
這時的享元工廠一半有兩個方法,一種用于提供單純享元對象,另一種用于提供復合享元對象。
public class CompositeConcreteFlyweight implements Flyweight {
private List<Flyweight> flyweights = new ArrayList<Flyweight>();
public void add(Flyweight flyweight){
flyweights.add(flyweight);
}
public void remove(Flyweight flyweight){
flyweights.remove(flyweight);
}
public void operation(String extrinsicState) {
for (Flyweight flyweight : flyweights){
flyweight.operation(extrinsicState);
}
}
}
public class FlyweightFactory {
//定義一個HashMap用于存儲享元對象,實現享元池
private static final Map<String, Flyweight> FLYWEIGHTS = new ConcurrentHashMap();
private static final FlyweightFactory INSTANCE = new FlyweightFactory();
private FlyweightFactory(){}
public static FlyweightFactory getInstance(){
return INSTANCE;
}
// 創建"復合享元"的工廠方法
public Flyweight getFlyweight(List<String> keys){
CompositeConcreteFlyweight compositeFly = new CompositeConcreteFlyweight();
int length = keys.size();
String key = null;
for (int i=0; i<length; i++) {
key = keys.get(i);
//調用"單純享元"的工廠方法
compositeFly.add(this.getFlyweight(key));
}
return compositeFly;
}
// 創建"單純享元"的工廠方法
public Flyweight getFlyweight(String key){
//如果對象存在,則直接從享元池獲取
if (FLYWEIGHTS.containsKey(key)){
return FLYWEIGHTS.get(key);
}else {
//如果對象不存在,先創建一個新的對象添加到享元池中,然后返回
Flyweight flyweight = new ConcreteFlyweight("intrinsicState");
FLYWEIGHTS.put(key, flyweight);
return flyweight;
}
}
}
七、模式擴展
享元模式通常需要和其他模式一起聯用,幾種常見的聯用方式如下:
- 在享元模式的享元工廠類中通常提供一個靜態的工廠方法用于返回享元對象,使用簡單工廠模式來生成享元對象。
- 在一個系統中,通常只有唯一一個享元工廠,因此可以使用單例模式進行享元工廠類的設計。
- 享元模式可以結合組合模式形成復合享元模式,統一對多個享元對象設置外部狀態。
八、優點和缺點
8.1、優點
享元模式的主要優點如下:
- 可以極大減少內存中對象的數量,使得相同或相似對象在內存中只保存一份,從而可以節約系統資源,提高系統性能。
- 享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元對象可以在不同的環境中被共享。
8.2、缺點
享元模式的主要缺點如下:
- 享元模式使得系統變得復雜,需要分離出內部狀態和外部狀態,這使得程序的邏輯復雜化。
- 為了使對象可以共享,享元模式需要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間變長。
九、適用環境
享元模式的使用頻率并不算太高,但是作為一種以“節約內存,提高性能”為出發點的設計模式,它在軟件開發中還是得到了一定程度的應用。
在以下情況下可以考慮使用享元模式:
- 一個系統有大量相同或者相似的對象,造成內存的大量耗費。
- 對象的大部分狀態都可以外部化,可以將這些外部狀態傳入對象中。
- 在使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,因此,應當在需要多次重復使用享元對象時才值得使用享元模式。
十、模式應用
JDK類庫中的String類使用了享元模式。
享元模式在編輯器軟件中大量使用,如在一個文檔中多次出現相同的圖片,則只需要創建一個圖片對象,通過在應用程序中設置該圖片出現的位置,可以實現該圖片在不同地方多次重復顯示。