C++设计模式编程之Flyweight享元模式结构详解

内容摘要
由遇到的问题引出享元模式:
在面向对象系统的设计何实现中,创建对象是最为常见的操作。这里面就有一个问题:如果一个应用程序使用了太多的对象,就会造成很大的存储开销。特别是
文章正文

由遇到的问题引出享元模式:

在面向对象系统的设计何实现中,创建对象是最为常见的操作。这里面就有一个问题:如果一个应用程序使用了太多的对象,就会造成很大的存储开销。特别是对于大量轻量级(细粒度)的对象,比如在文档编辑器的设计过程中,我们如果为没有字母创建一个对象的话,系统可能会因为大量的对象而造成存储开销的浪费。例如一个字母"a"在文档中出现了100000 次,而实际上我们可以让这一万个字母"a"共享一个对象,当然因为在不同的位置可能字母"a"有不同的显示效果(例如字体和大小等设置不同),在这种情况我们可以为将对象的状态分为"外部状态"和"内部状态",将可以被共享(不会变化)的状态作为内部状态存储在对象中,而外部对象(例如上面提到的字体、大小等)我们可以在适当的时候将外部对象最为参数传递给对象(例如在显示的时候,将字体、大小等信息传递给对象)。

作用:运用共享技术有效地支持大量细粒度的对象。

内部状态intrinsic和外部状态extrinsic:

1)Flyweight模式中,最重要的是将对象分解成intrinsic和extrinsic两部分。

2)内部状态:在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态

3)外部状态:而随环境改变而改变的,取决于应用环境,或是实时数据,这些不可以共享的东西就是外部状态了。

4)内部状态和外部状态之间的区别:
  在Flyweight模式应用中,通常修改的是外部状态属性,而内部状态属性一般都是用于参考或计算时引用。
Flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部状态则由Client对象存储或计算。当用户调用Flyweight对象的操作时,将该状态传递给它。

以文字处理软件为例:

  内部状态存储于flyweight中,它包含了独立于flyweight场景的信息,这些信息使得flyweight可以被共享。如字符代码,字符大小……

  外部状态取决于flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给flyweight,如字符位置,字符颜色……

UML图:

解析:
Flyweight:享元类的基类,定义一个接口,通过这个接口Flyweight可以接受并作用于外部状态。

ConcreteFlyweight:实现Flyweight接口, 并为内部状态( 如果有的话) 增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的(intrinsic);即,它必须独立于ConcreteFlyweight对象的场景。

UnsharedConcreteFlyweight:并非所有的Flyweight子类都需要被共享。Flyweight接口使共享成为可能,但它并不强制共享。在Flyweight对象结构的某些层次,UnsharedConcreteFlyweight对象通常将ConcreteFlyweight对象作为子节点。

FlyweightFactory:

1) 创建并管理Flyweight对象。

2)确保合理地共享Flyweight。当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)

Client
1)维持一个对Flyweight的引用。

2)计算或存储一个(多个)Flyweight的外部状态。

分析:
   享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例数据除了几个参数外基本都是相同的。有时就能够大幅度地减少实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。

  比如在文档编辑器的设计过程中,我们如果为没有字母创建一个对象的话,系统可能会因为大量的对象而造成存储开销的浪费。例如一个字母“a”在文档中出现了100000次,而实际上我们可以让这一万个字母“a”共享一个对象,当然因为在不同的位置可能字母“a”有不同的显示效果(例如字体和大小等设置不同),在这种情况我们可以为将对象的状态分为“外部状态”和“内部状态”,将可以被共享(不会变化)的状态作为内部状态存储在对象中,而外部对象(例如上面提到的字体、大小等)我们可以在适当的时候将外部对象最为参数传递给对象(例如在显示的时候,将字体、大小等信息传递给对象)。

  Flyweight的内部状态是用来共享的,Flyweightfactory负责维护一个Flyweight池(存放内部状态的对象),当客户端请求一个共享Flyweight时,这个factory首先搜索池中是否已经有可适用的,如果有,factory只是简单返回送出这个对象,否则,创建一个新的对象,加入到池中,再返回送出这个对象.池为重复或可共享的对象、属性设置一个缓冲,称为内部状态。这些内部状态一般情况下都是不可修改的,也就是在第一个对象、属性被创建后,就不会去修改了(否则就没意义了)。

  Flyweight 对对象的内部状态进行共享,只为每种内部状态创建一个实例,对内部状态使用了单例模式。

  用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。

  存储节约由以下几个因素决定:
  1) 因为共享,实例总数减少的数目
  2) 对象内部状态的平均数目
  3) 外部状态是计算的还是存储的

实现要点
1)享元工厂维护一张享元实例表。

2)享元不可共享的状态需要在外部维护。即删除外部状态:该模式的可用性在很大程度上取决于是否容易识别外部状态并将它从共享对象中删除。

3)按照需求可以对享元角色进行抽象。

4)管理共享对象:引用计数和垃圾回收……

何时采用
1)如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;

2)还有就是对象的大多数状态可变为外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑所使用享元模式。

3)系统中有大量耗费了大量内存的细粒度对象,并且对外界来说这些对没有任何差别的(或者说经过改造后可以是没有差别的)。

  在文档编辑器例子中如果一个字符对应一个对象,那么一篇文档所要容纳的对象将是非常的庞大耗费大量的内存。而实际组成文档的字符是有限的,是由这些字符不同的组合和排列得到的。所以需要共享,将基本的字符进行共享,将使得字符对象变得有限。

示例:
享元模式在编辑器系统中大量使用。一个文本编辑器往往会提供很多种字体,而通常的做法就是将每一个字母做成一个享元对象。享元对象的内蕴状态就是这个字母,而字母在文本中的位置和字模风格等其他信息则是外蕴状态。比如,字母a可能出现在文本的很多地方,虽然这些字母a的位置和字模风格不同,但是所有这些地方使用的都是同一个字母对象。这样一来,字母对象就可以在整个系统中共享。

// Flyweight pattern -- Real World example 


using System;
using System.Collections;

namespace DoFactory.GangOfFour.Flyweight.RealWorld
{

 // MainApp test application 

 class MainApp
 {
  static void Main()
  {
   // Build a document with text 
   string document = "AAZZBBZB";
   char[] chars = document.ToCharArray();

   CharacterFactory f = new CharacterFactory();

   // extrinsic state 
   int pointSize = 10;

   // For each character use a flyweight object 
   foreach (char c in chars)
   {
    pointSize++;
    Character character = f.GetCharacter(c);
    character.Display(pointSize);
   }

   // Wait for user 
   Console.Read();
  }
 }

 // "FlyweightFactory" 

 class CharacterFactory
 {
  private Hashtable characters = new Hashtable();

  public Character GetCharacter(char key)
  {
   // Uses "lazy initialization" 
   Character character = characters[key] as Character;
   if (character == null)
   {
    switch (key)
    {
     case 'A': character = new CharacterA(); break;
     case 'B': character = new CharacterB(); break;
      // 
     case 'Z': character = new CharacterZ(); break;
    }
    characters.Add(key, character);
   }
   return character;
  }
 }

 // "Flyweight" 

 abstract class Character
 {
  protected char symbol;
  protected int width;
  protected int height;
  protected int ascent;
  protected int descent;
  protected int pointSize;

  public abstract void Display(int pointSize);
 }

 // "ConcreteFlyweight" 

 class CharacterA : Character
 {
  // Constructor 
  public CharacterA()
  {
   this.symbol = 'A';
   this.height = 100;
   this.width = 120;
   this.ascent = 70;
   this.descent = 0;
  }

  public override void Display(int pointSize)
  {
   this.pointSize = pointSize;
   Console.WriteLine(this.symbol + 
    " (pointsize " + this.pointSize + ")");
  }
 }

 // "ConcreteFlyweight" 

 class CharacterB : Character
 {
  // Constructor 
  public CharacterB()
  {
   this.symbol = 'B';
   this.height = 100;
   this.width = 140;
   this.ascent = 72;
   this.descent = 0;
  }

  public override void Display(int pointSize)
  {
   this.pointSize = pointSize;
   Console.WriteLine(this.symbol + 
    " (pointsize " + this.pointSize + ")");
  }

 }

 // C, D, E, etc. 

 // "ConcreteFlyweight" 

 class CharacterZ : Character
 {
  // Constructor 
  public CharacterZ()
  {
   this.symbol = 'Z';
   this.height = 100;
   this.width = 100;
   this.ascent = 68;
   this.descent = 0;
  }

  public override void Display(int pointSize)
  {
   this.pointSize = pointSize;
   Console.WriteLine(this.symbol + 
    " (pointsize " + this.pointSize + ")");
  }
 }
}

Output:

享元模式的优点和缺点

享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:
享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。


代码注释

作者:喵哥笔记

IDC笔记

学的不仅是技术,更是梦想!