天天看點

面向對象設計的設計模式(十二):享元模式

面向對象設計的設計模式(十二):享元模式

定義

享元模式(Flyweight Pattern):運用共享技術複用大量細粒度的對象,降低程式記憶體的占用,提高程式的性能。

定義解讀:

  • 享元模式的目的就是使用共享技術來實作大量細粒度對象的複用,提高性能。
  • 享元對象能做到共享的關鍵是區分内部狀态(Internal State)和外部狀态(External State)。
  • 内部狀态是存儲在享元對象内部并且不會随環境改變而改變的狀态,是以内部狀态可以共享。
  • 外部狀态是随環境改變而改變的、不可以共享的狀态。享元對象的外部狀态必須由用戶端儲存,并在享元對象被建立之後,在需要使用的時候再傳入到享元對象内部。一個外部狀态與另一個外部狀态之間是互相獨立的。

适用場景

  • 系統有大量的相似對象,這些對象有一些外在狀态。
  • 應當在多次重複使用享元對象時才值得使用享元模式。使用享元模式需要維護一個存儲享元對象的享元池,而這需要耗費資源,是以,

成員與類圖

成員

享元模式一共有三個成員:

  • 享元工廠(FlyweightFactory): 享元工廠提供一個用于存儲享元對象的享元池,使用者需要對象時,首先從享元池中擷取,如果享元池中不存在,則建立一個新的享元對象傳回給使用者,并在享元池中儲存該新增對象
  • 抽象享元(Flyweight):抽象享元定義了具體享元對象需要實作的接口。
  • 具體享元(ConcreteFlyweight): 具體享元實作了抽象享元類定義的接口。

模式類圖

面向對象設計的設計模式(十二):享元模式

享元模式類圖

代碼示例

場景概述

這裡我們使用《Objective-C 程式設計之道:iOS設計模式解析》裡的第21章使用的例子:在一個頁面展示數百個大小,位置不同的花的圖檔,然而這些花的樣式隻有6種。

看一下截圖:

面向對象設計的設計模式(十二):享元模式

百花圖

場景分析

由于這裡我們需要建立很多對象,而這些對象有可以共享的内部狀态(6種圖檔内容)以及不同的外部狀态(随機的,數百個位置坐标和圖檔大小),是以比較适合使用享元模式來做。

根據上面提到的享元模式的成員:

  • 我們需要建立一個工廠類來根據花的類型來傳回花對象(這個對象包括内部可以共享的圖檔以及外部狀态位置和大小):每次當新生成一種花的類型的對象的時候就把它儲存起來,因為下次如果還需要這個類型的花内部圖檔對象的時候就可以直接用了。
  • 抽象享元類就是Objective-C的原生

    UIImageView

    ,它可以顯示圖檔
  • 具體享元類可以自己定義一個類繼承于

    UIImageView

    ,因為後續我們可以直接添加更多其他的屬性。

下面我們看一下用代碼如何實作:

代碼實作

首先我們建立一個工廠,這個工廠可以根據所傳入花的類型來傳回花内部圖檔對象,在這裡可以直接使用原生的

UIImage

對象,也就是圖檔對象。而且這個工廠持有一個儲存圖檔對象的池子:

  • 當該類型的花第一次被建立時,工廠會建立一個所對應的花内部圖檔對象,并将這個對象放入池子中儲存起來。
  • 當該類型的花内部圖檔對象在池子裡已經有了,那麼工廠則直接從池子裡傳回這個花内部圖檔對象。

下面我們看一下代碼是如何實作的:

//================== FlowerFactory.h ================== 

typedef enum 
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes

} FlowerType;

@interface FlowerFactory : NSObject 

- (FlowerImageView *) flowerImageWithType:(FlowerType)type

@end




//================== FlowerFactory.m ================== 

@implementation FlowerFactory
{
    NSMutableDictionary *_flowersPool;
}

- (FlowerImageView *) flowerImageWithType:(FlowerType)type
{

  if (_flowersPool == nil){

     _flowersPool = [[NSMutableDictionary alloc] initWithCapacity:kTotalNumberOfFlowerTypes];
  }

  //嘗試擷取傳入類型對應的花内部圖檔對象
  UIImage *flowerImage = [_flowersPool objectForKey:[NSNumber numberWithInt:type]];

  //如果沒有對應類型的圖檔,則生成一個
  if (flowerImage == nil){

    NSLog(@"create new flower image with type:%u",type);

    switch (type){

      case kAnemone:
        flowerImage = [UIImage imageNamed:@"anemone.png"];
        break;
      case kCosmos:
        flowerImage = [UIImage imageNamed:@"cosmos.png"];
        break;
      case kGerberas:
        flowerImage = [UIImage imageNamed:@"gerberas.png"];
        break;
      case kHollyhock:
        flowerImage = [UIImage imageNamed:@"hollyhock.png"];
        break;
      case kJasmine:
        flowerImage = [UIImage imageNamed:@"jasmine.png"];
        break;
      case kZinnia:
        flowerImage = [UIImage imageNamed:@"zinnia.png"];
        break;
      default:
        flowerImage = nil;
        break;

    }

    [_flowersPool setObject:flowerImage forKey:[NSNumber numberWithInt:type]];

  }else{
      //如果有對應類型的圖檔,則直接使用
      NSLog(@"reuse flower image with type:%u",type);
  }

  //建立花對象,将上面拿到的花内部圖檔對象指派并傳回
  FlowerImageView *flowerImageView = [[FlowerImageView alloc] initWithImage:flowerImage];

  return flowerImageView;
}           

複制

  • 在這個工廠類裡面定義了六中圖檔的類型
  • 該工廠類持有

    _flowersPool

    私有成員變量,儲存新建立過的圖檔。
  • flowerImageWithType:

    的實作:結合了

    _flowersPool

    :當

    _flowersPool

    沒有對應的圖檔時,新建立圖檔并傳回;否則直接從

    _flowersPool

    擷取對應的圖檔并傳回。

接着我們定義這些花對象

FlowerImageView

//================== FlowerImageView.h ================== 

@interface FlowerImageView : UIImageView 

@end


//================== FlowerImageView.m ================== 

@implementation FlowerImageView

@end           

複制

在這裡面其實也可以直接使用

UIImageView

,之是以建立一個子類是為了後面可以更好地擴充這些花獨有的一些屬性。

注意一下花對象和花内部圖檔對象的差別:花對象

FlowerImageView

是包含花内部圖檔對象的

UIImage

。因為在Objective-C裡面,

UIImage

FlowerImageView

所繼承的

UIImageView

的一個屬性,是以在這裡

FlowerImageView

就直接包含了

UIImage

下面我們來看一下用戶端如何使用

FlowerFactory

FlowerImageView

這兩個類:

//================== client ================== 

//首先建造一個生産花内部圖檔對象的工廠
FlowerFactory *factory = [[FlowerFactory alloc] init];

for (int i = 0; i < 500; ++i)
{
    //随機傳入一個花的類型,讓工廠傳回該類型對應花類型的花對象
    FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
    FlowerImageView *flowerImageView = [factory flowerImageWithType:flowerType];

    // 建立花對象的外部屬性值(随機的位置和大小)
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
    CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
    NSInteger minSize = 10;
    NSInteger maxSize = 50;
    CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;

    //将位置和大小賦予花對象
    flowerImageView.frame = CGRectMake(x, y, size, size);

    //展示這個花對象
    [self.view addSubview:flowerImageView];
}           

複制

上面代碼裡面是生成了500朵位置和大小都是随機的花内部圖檔對象。這500朵花最主要的差別還是它們的位置和大小;而它們使用的花的圖檔對象隻有6個,是以可以用專門的

Factory

來生成和管理這些少數的花内部圖檔對象,從工廠的列印我們可以看出來:

create new flower image with type:1
create new flower image with type:3
create new flower image with type:4
reuse flower image with type:3
create new flower image with type:5
create new flower image with type:2
create new flower image with type:0
reuse flower image with type:5
reuse flower image with type:5
reuse flower image with type:4
reuse flower image with type:1
reuse flower image with type:3
reuse flower image with type:4
reuse flower image with type:0           

複制

從上面的列印結果可以看出,在六種圖檔都建立好以後,再擷取時就直接拿生成過的圖檔了,在一定程度上減少了記憶體的開銷。

下面我們來看一下該代碼示例對應的UML類圖。

代碼對應的類圖

面向對象設計的設計模式(十二):享元模式

享元模式代碼示例類圖

這裡需要注意的是
  • 工廠和花對象是組合關系:

    FlowerFactroy

    生成了多個

    FlowerImageView

    對象,也就是花的内部圖檔對象,二者的關系屬于強關系,因為在該例子中二者如果分離而獨立存在都将會失去意義,是以在UML類圖中用了組合的關系(實心菱形)。
  • 抽象享元類是

    UIImageView

    ,它的一個内部對象是

    UIImage

    (這兩個都是Objective-C原生的關于圖檔的類)。
  • 用戶端依賴的對象是工廠對象和花對象,而對花的内部圖檔對象

    UIImage

    可以一無所知,因為它是被

    FlowerFactroy

    建立并被

    FlowerImageView

    所持有的。(但是因為

    UIImage

    FlowerImageView

    的一個外部可以引用的屬性,是以在這裡用戶端還是可以通路到

    UIImage

    ,這是Objective-C原生的實作。後面我們在用享元模式的時候可以不将内部屬性暴露出來)

優點

  • 使用享元模可以減少記憶體中對象的數量,使得相同對象或相似對象在記憶體中隻儲存一份,降低系統的使用記憶體,也可以提性能。
  • 享元模式的外部狀态相對獨立,而且不會影響其内部狀态,進而使得享元對象可以在不同的環境中被共享。

缺點

  • 使用享元模式需要分離出内部狀态和外部狀态,這使得程式的邏輯複雜化。
  • 對象在緩沖池中的複用需要考慮線程問題。

Objective-C & Java的實踐

  • iOS SDK中的

    UITableViewCell

    的複用池就是使用享元模式的一個例子。
  • Java:JDK中的

    Integer

    類的

    valueOf

    方法,如果傳入的值的區間在

    [IntegerCache.low,IntegerCache.high]

    中的話,則直接從緩存裡擷取;否則就建立一個新的

    Integer