面向对象设计的设计模式(十二):享元模式

2019-05-06 14:16:53 浏览数 (1)

定义

享元模式(Flyweight Pattern):运用共享技术复用大量细粒度的对象,降低程序内存的占用,提高程序的性能。

定义解读:

  • 享元模式的目的就是使用共享技术来实现大量细粒度对象的复用,提高性能。
  • 享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
  • 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
  • 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。

适用场景

  • 系统有大量的相似对象,这些对象有一些外在状态。
  • 应当在多次重复使用享元对象时才值得使用享元模式。使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,

成员与类图

成员

享元模式一共有三个成员:

  • 享元工厂(FlyweightFactory): 享元工厂提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象
  • 抽象享元(Flyweight):抽象享元定义了具体享元对象需要实现的接口。
  • 具体享元(ConcreteFlyweight): 具体享元实现了抽象享元类定义的接口。

模式类图

享元模式类图

代码示例

场景概述

这里我们使用《Objective-C 编程之道:iOS设计模式解析》里的第21章使用的例子:在一个页面展示数百个大小,位置不同的花的图片,然而这些花的样式只有6种。

看一下截图:

百花图

场景分析

由于这里我们需要创建很多对象,而这些对象有可以共享的内部状态(6种图片内容)以及不同的外部状态(随机的,数百个位置坐标和图片大小),因此比较适合使用享元模式来做。

根据上面提到的享元模式的成员:

  • 我们需要创建一个工厂类来根据花的类型来返回花对象(这个对象包括内部可以共享的图片以及外部状态位置和大小):每次当新生成一种花的类型的对象的时候就把它保存起来,因为下次如果还需要这个类型的花内部图片对象的时候就可以直接用了。
  • 抽象享元类就是Objective-C的原生UIImageView,它可以显示图片
  • 具体享元类可以自己定义一个类继承于UIImageView,因为后续我们可以直接添加更多其他的属性。

下面我们看一下用代码如何实现:

代码实现

首先我们创建一个工厂,这个工厂可以根据所传入花的类型来返回花内部图片对象,在这里可以直接使用原生的UIImage对象,也就是图片对象。而且这个工厂持有一个保存图片对象的池子:

  • 当该类型的花第一次被创建时,工厂会新建一个所对应的花内部图片对象,并将这个对象放入池子中保存起来。
  • 当该类型的花内部图片对象在池子里已经有了,那么工厂则直接从池子里返回这个花内部图片对象。

下面我们看一下代码是如何实现的:

代码语言:javascript复制
//================== 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

代码语言:javascript复制
//================== FlowerImageView.h ================== 

@interface FlowerImageView : UIImageView 

@end


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

@implementation FlowerImageView

@end

在这里面其实也可以直接使用UIImageView,之所以创建一个子类是为了后面可以更好地扩展这些花独有的一些属性。 注意一下花对象和花内部图片对象的区别:花对象FlowerImageView是包含花内部图片对象的UIImage。因为在Objective-C里面,UIImageFlowerImageView所继承的UIImageView的一个属性,所以在这里FlowerImageView就直接包含了UIImage

下面我们来看一下客户端如何使用FlowerFactoryFlowerImageView这两个类:

代码语言:javascript复制
//================== 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来生成和管理这些少数的花内部图片对象,从工厂的打印我们可以看出来:

代码语言:javascript复制
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所持有的。(但是因为UIImageFlowerImageView的一个外部可以引用的属性,所以在这里客户端还是可以访问到UIImage,这是Objective-C原生的实现。后面我们在用享元模式的时候可以不将内部属性暴露出来)

优点

  • 使用享元模可以减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份,降低系统的使用内存,也可以提性能。
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

缺点

  • 使用享元模式需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
  • 对象在缓冲池中的复用需要考虑线程问题。

Objective-C & Java的实践

  • iOS SDK中的UITableViewCell的复用池就是使用享元模式的一个例子。
  • Java:JDK中的Integer类的valueOf方法,如果传入的值的区间在[IntegerCache.low,IntegerCache.high]中的话,则直接从缓存里获取;否则就创建一个新的Integer

0 人点赞