First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 2960 评论 :: 298 Trackbacks

一、 享元(Flyweight)模式

Flyweight在拳击比赛中指最轻量级,即"蝇量级",有些作者翻译为"羽量级"。这里使用"享元模式"更能反映模式的用意。

享元模式以共享的方式高效地支持大量的细粒度对象。享元对象能做到共享的关键是区分内蕴状态(Internal State)和外蕴状态(External State)。内蕴状态是存储在享元对象内部并且不会随环境改变而改变。因此内蕴状态并可以共享。

外蕴状态是随环境改变而改变的、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。外蕴状态与内蕴状态是相互独立的。

享元模式的应用

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


二、 单纯享元模式的结构

在单纯享元模式中,所有的享元对象都是可以共享的。单纯享元模式所涉及的角色如下:

抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态(External State)的操作可以通过调用商业方法以参数形式传入。

具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。

享元工厂(FlyweightFactory)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。

客户端(Client)角色:本角色需要维护一个对所有享元对象的引用。本角色需要自行存储所有享元对象的外蕴状态。


三、 单纯享元模式的示意性源代码

// Flyweight pattern -- Structural example  
using System;
using System.Collections;

// "FlyweightFactory"
class FlyweightFactory
{
  
// Fields
  private Hashtable flyweights = new Hashtable();

  
// Constructors
  public FlyweightFactory()
  
{
    flyweights.Add(
"X"new ConcreteFlyweight());
    flyweights.Add(
"Y"new ConcreteFlyweight());
    flyweights.Add(
"Z"new ConcreteFlyweight());
  }


  
// Methods
  public Flyweight GetFlyweight(string key)
  
{
    
return((Flyweight)flyweights[ key ]);
  }

}


// "Flyweight"
abstract class Flyweight
{
  
// Methods
  abstract public void Operation( int extrinsicstate );
}


// "ConcreteFlyweight"
class ConcreteFlyweight : Flyweight
{
  
private string intrinsicstate = "A";
  
// Methods
  override public void Operation( int extrinsicstate )
  
{
    Console.WriteLine(
"ConcreteFlyweight: intrinsicstate {0}, extrinsicstate {1}"
      intrinsicstate, extrinsicstate );
  }

}


/// <summary>
/// Client test
/// </summary>

public class Client
{
  
public static void Main( string[] args )
  
{
    
// Arbitrary extrisic state
    int extrinsicstate = 22;
     
    FlyweightFactory f 
= new FlyweightFactory();

    
// Work with different flyweight instances
    Flyweight fx = f.GetFlyweight("X");
    fx.Operation( 
--extrinsicstate );

    Flyweight fy 
= f.GetFlyweight("Y");
    fy.Operation( 
--extrinsicstate );

    Flyweight fz 
= f.GetFlyweight("Z");
    fz.Operation( 
--extrinsicstate );
  }

}


四、 复合享元模式的结构

单纯享元模式中,所有的享元对象都可以直接共享。下面考虑一个较为复杂的情况,即将一些单纯享元使用合成模式加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。

复合享元模式的类图如下图所示:

享元模式所涉及的角色有抽象享元角色、具体享元角色、复合享元角色、享员工厂角色,以及客户端角色等。

抽象享元角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。

具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。

复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。

享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。

客户端(Client)角色:本角色还需要自行存储所有享元对象的外蕴状态。

注:由于复合享元模式比较复杂,这里就不再给出示意性代码。通过将享元模式与合成模式组合在一起,可以确保复合享元中所包含的每个单纯享元都具有相同的外蕴状态,而这些单纯享元的内蕴状态往往不同。该部分内容可以参考《Java与模式》第31章内容。


五、 一个咖啡摊的例子

在这个咖啡摊(Coffee Stall)所使用的系统里,有一系列的咖啡"风味(Flavor)"。客人到摊位上购买咖啡,所有的咖啡均放在台子上,客人自己拿到咖啡后就离开摊位。咖啡有内蕴状态,也就是咖啡的风味;咖啡没有环境因素,也就是说没有外蕴状态。如果系统为每一杯咖啡都创建一个独立的对象的话,那么就需要创建出很多的细小对象来。这样就不如把咖啡按照种类(即"风味")划分,每一种风味的咖啡只创建一个对象,并实行共享。

使用咖啡摊主的语言来讲,所有的咖啡都可按"风味"划分成如Capucino、Espresso等,每一种风味的咖啡不论卖出多少杯,都是全同、不可分辨的。所谓共享,就是咖啡风味的共享,制造方法的共享等。因此,享元模式对咖啡摊来说,就意味着不需要为每一份单独调制。摊主可以在需要时,一次性地调制出足够一天出售的某一种风味的咖啡。

很显然,这里适合使用单纯享元模式。系统的设计如下:

using System;
using System.Collections;

public abstract class Order
{
  
// 将咖啡卖给客人
  public abstract void Serve();
  
// 返回咖啡的名字
  public abstract string GetFlavor();
}


public class Flavor : Order
{
  
private string flavor;

  
// 构造函数,内蕴状态以参数方式传入
  public Flavor(string flavor)
  
{
    
this.flavor = flavor;
  }


  
// 返回咖啡的名字
  public override string GetFlavor()
  
{
    
return this.flavor;
  }


  
// 将咖啡卖给客人
  public override void Serve()
  
{
    Console.WriteLine(
"Serving flavor " + flavor);
  }

}


public class FlavorFactory
{
  
private Hashtable flavors = new Hashtable();

  
public Order GetOrder(string key)
  
{
    
if(! flavors.ContainsKey(key))
      flavors.Add(key, 
new Flavor(key));

        
return ((Order)flavors[key]);
  }


  
public int GetTotalFlavorsMade()
  
{
    
return flavors.Count;
  }

}


public class Client
{
  
private static FlavorFactory flavorFactory;
  
private static int ordersMade = 0;

  
public static void Main( string[] args )
  
{
    flavorFactory 
= new FlavorFactory();

    TakeOrder(
"Black Coffee");
    TakeOrder(
"Capucino");
    TakeOrder(
"Espresso");
    TakeOrder(
"Capucino");
    TakeOrder(
"Espresso");
    TakeOrder(
"Black Coffee");
    TakeOrder(
"Espresso");
    TakeOrder(
"Espresso");
    TakeOrder(
"Black Coffee");
    TakeOrder(
"Capucino");
    TakeOrder(
"Capucino");
    TakeOrder(
"Black Coffee");

    Console.WriteLine(
"\nTotal Orders made: " + ordersMade);

    Console.WriteLine(
"\nTotal Flavor objects made: " + 
      flavorFactory.GetTotalFlavorsMade());
  }


  
private static void TakeOrder(string aFlavor)
  
{
    Order o 
= flavorFactory.GetOrder(aFlavor);
    
// 将咖啡卖给客人
    o.Serve();

    ordersMade
++;
  }

}


六、 咖啡屋的例子

在前面的咖啡摊项目里,由于没有供客人坐的桌子,所有的咖啡均没有环境的影响。换言之,咖啡仅有内蕴状态,也就是咖啡的种类,而没有外蕴状态。

下面考虑一个规模稍稍大一点的咖啡屋(Coffee Shop)项目。屋子里有很多的桌子供客人坐,系统除了需要提供咖啡的"风味"之外,还需要跟踪咖啡被送到哪一个桌位上,因此,咖啡就有了桌子作为外蕴状态。

由于外蕴状态的存在,没有外蕴状态的单纯享元模式不再符合要求。系统的设计可以利用有外蕴状态的单纯享元模式。系统的代码如下:

using System;
using System.Collections;

public abstract class Order
{
  
// 将咖啡卖给客人
  public abstract void Serve(Table table);
  
// 返回咖啡的名字
  public abstract string GetFlavor();
}


public class Flavor : Order
{
  
private string flavor;

  
// 构造函数,内蕴状态以参数方式传入
  public Flavor(string flavor)
  
{
    
this.flavor = flavor;
  }


  
// 返回咖啡的名字
  public override string GetFlavor()
  
{
    
return this.flavor;
  }


  
// 将咖啡卖给客人
  public override void Serve(Table table)
  
{
    Console.WriteLine(
"Serving table {0} with flavor {1}", table.Number, flavor);
  }

}


public class FlavorFactory
{
  
private Hashtable flavors = new Hashtable();

  
public Order GetOrder(string key)
  
{
    
if(! flavors.ContainsKey(key))
      flavors.Add(key, 
new Flavor(key));

        
return ((Order)flavors[key]);
  }


  
public int GetTotalFlavorsMade()
  
{
    
return flavors.Count;
  }

}


public class Table
{
  
private int number;

  
public Table(int number)
  
{
    
this.number = number;
  }


  
public int Number
  
{
    
get return number; }
  }

}


public class Client
{
  
private static FlavorFactory flavorFactory;
  
private static int ordersMade = 0;

  
public static void Main( string[] args )
  
{
    flavorFactory 
= new FlavorFactory();

    TakeOrder(
"Black Coffee");
    TakeOrder(
"Capucino");
    TakeOrder(
"Espresso");
    TakeOrder(
"Capucino");
    TakeOrder(
"Espresso");
    TakeOrder(
"Black Coffee");
    TakeOrder(
"Espresso");
    TakeOrder(
"Espresso");
    TakeOrder(
"Black Coffee");
    TakeOrder(
"Capucino");
    TakeOrder(
"Capucino");
    TakeOrder(
"Black Coffee");

    Console.WriteLine(
"\nTotal Orders made: " + ordersMade);

    Console.WriteLine(
"\nTotal Flavor objects made: " + 
      flavorFactory.GetTotalFlavorsMade());
  }


  
private static void TakeOrder(string aFlavor)
  
{
    Order o 
= flavorFactory.GetOrder(aFlavor);
    
    
// 将咖啡卖给客人
    o.Serve(new Table(++ordersMade));
  }

}

 

七、 享元模式应当在什么情况下使用

当以下所有的条件都满足时,可以考虑使用享元模式:

  • 一个系统有大量的对象。
  • 这些对象耗费大量的内存。
  • 这些对象的状态中的大部分都可以外部化。
  • 这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。
  • 软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。

满足以上的这些条件的系统可以使用享元对象。

最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。


八、 享元模式的优点和缺点

享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:

  • 享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
  • 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。



参考文献:
阎宏,《Java与模式》,电子工业出版社
[美]James W. Cooper,《C#设计模式》,电子工业出版社
[美]Alan Shalloway  James R. Trott,《Design Patterns Explained》,中国电力出版社
[美]Robert C. Martin,《敏捷软件开发-原则、模式与实践》,清华大学出版社
[美]Don Box, Chris Sells,《.NET本质论 第1卷:公共语言运行库》,中国电力出版社

 

posted on 2004-10-23 12:07 吕震宇 阅读(10167) 评论(27)  编辑 收藏 所属分类: 设计模式

评论

#1楼  2004-10-23 16:15 cnlamar [未注册用户]
没认真看,其思想是不是将对象的状态和对象本身分离?
  回复  引用    

#2楼 [楼主] 2004-10-23 19:30 吕震宇      
将内部状态(不会发生变化的状态)与外部状态(可能发生变化的状态)分离,这样不会发生变化的部分可以抽取出来共享,而可能发生变化的部分由客户维护。
  回复  引用  查看    

#3楼  2004-10-24 00:32 msolap      
其思想是不是将对象的状态和对象本身分离? -- 问得妙啊,而吕兄的回答更是画龙点睛!
  回复  引用  查看    

#4楼  2004-10-24 00:43 wayfarer      
我整个儿订阅了设计模式分类的Rss,谢谢了啊:)
  回复  引用  查看    

#5楼  2004-12-21 09:06 fuleyou [未注册用户]
底下写了这么多的参考文献,看起来好是NB,但我怎么发现和Java与模式上的很是相同阿,只是换汤不换药的改了下代码。佩服楼主!!
  回复  引用    

#6楼  2004-12-21 12:15 81      
不要在乎别人的评论,即使在乎别人的评论,但是也只有N:1,
你认为呢?

即使只是更改代码,把不好理解的代码改为平易近人的代码不是一件值得称赞的功劳吗!
  回复  引用  查看    

#7楼  2004-12-21 13:04 泡茶 [未注册用户]
恩,以前看多多本设计模式方面的书,有时是因为英语水平太差,有时是因为书中的艰深文字,自己对很多个模式并没有真正的理解。吕兄通过简单浅显的例子作以解释,受益良多,希望吕兄能够坚持下去。
  回复  引用    

#8楼  2004-12-21 15:19 haha [未注册用户]
吕兄,我找了几本跟模式沾边的书,你用不用放到你的参考文献中去,一定倍有面子!!
  回复  引用    

#9楼 [楼主] 2004-12-21 18:39 吕震宇      
@haha

想问一句你有没有读过那些参考文献?我读过,每一本都读过。
再问一句你有没有读过我写的设计模式系列的所有文章?如果没有的化,你怎么知道我没有参考这些书?
最后问一句写参考文献的目的是为了挣面子吗?如果你真这样想的化,我想就不是用可笑两个字可以形容的了!

哈哈!
  回复  引用  查看    

#10楼  2004-12-21 19:48 heihei [未注册用户]
那就劳烦吕兄把引用的地方给列出来,大家的眼睛是雪亮的!!
支持你!!
  回复  引用    

#11楼  2004-12-21 21:12 吕震宇      
@heihei

不知道你是不是只读到了《设计模式14》,去读读《设计模式(16)-Bridge Pattern》吧。另外其它的内容还是从《设计模式1》开始,很快就能看到的。
  回复  引用  查看    

#12楼  2004-12-22 21:54 xiaochengyong      
支持吕兄。 第一次从图书馆借了《Java与模式》,看着它1024页的厚度,没有怎么认真看就还了。当我看了这个系列后,受益良多。于是又赶紧到图书馆将《Java与模式》借出,细加研读,倍感亲切。希望吕兄能够坚持下去。这个系列我作为经典收藏了。
  回复  引用  查看    

#13楼  2005-01-10 11:00 收藏 [未注册用户]
收藏
  回复  引用    

#14楼  2005-06-29 09:44 cv222 [未注册用户]
内容来自哪里不重要, 重要的是能把道理讲清楚, 吕兄的努力是绝对值得肯定的, 总比你去肯设计模式那本书来的快(书就在我边上, 看看就痛苦)
  回复  引用    

#15楼  2005-10-31 20:02 ls0627 [未注册用户]
抽象享元角色有什么用,似乎不需要。
  回复  引用    

愿意把自己的收获与别人分享是值得尊敬的,而且可以把重要的内容整理出来也是令人佩服的。
  回复  引用    

#17楼  2006-04-05 13:31 zitiger      
支持吕兄。
  回复  引用  查看    

#18楼  2006-04-05 14:14 zitiger      
是不是可以这么理解,对于经常且大量要用到的,又相似的实例,把他们放到列表中,下次要用的时候从表里取得就可以了.

问一下,这里的咖啡摊可以用原型模式吗?
因为一杯咖啡卖出去之后用户可能有可能会改变原来咖啡的属性,如加些糖之类的,这样原来的咖啡属性就会改变了.
  回复  引用  查看    

我觉得不需要用原型模式,因为不需要改变初始化的属性。原型模式是在创建对象时可以保留原始对象的状态,而这里每杯咖啡被创建时肯定都是原始默认的状态。所以直接用new就可以了。
  回复  引用    

#20楼  2006-09-28 11:08 沙漠野狼[匿名]      
吕老师:我有个问题,请帮解答!
我突发奇想的把(三、 单纯享元模式的示意性源代码)的五个地方改了一下,如下注释,运行后是可以,我不知为什么?
class Program
{
//FlyWeightFactory
class FlyweightFactory
{
//Fields
private Hashtable flyweights = new Hashtable();

//Constructors
public FlyweightFactory()
{
flyweights.Add("X",new ConcreteFlyweight());
flyweights.Add("Y", new ConcreteFlyweight());
flyweights.Add("Z", new ConcreteFlyweight());
}

//Methods
public Flyweight GetFlyweight(string Key)//1-把这里的Flyweight换成ConcreteFlyweight
{
return (Flyweight)flyweights[Key];//2-把这里的Flyweight换成ConcreteFlyweight
}
}

//Flyweight
abstract class Flyweight
{
//Methods
abstract public void Operation(int extrinsicstate);
}

//ConcreteFlyweight
class ConcreteFlyweight : Flyweight
{
private string intrinsicstate="A";

public override void Operation(int extrinsicstate)
{
Console.WriteLine("ConcreteFlyweight:intrinsicstate {0},extrinsicstate {1}",intrinsicstate,extrinsicstate);
}
}

static void Main(string[] args)
{
int extrinsicstate = 22;
FlyweightFactory f = new FlyweightFactory();

Flyweight fX = f.GetFlyweight("X"); //3-把这里的Flyweight换成ConcreteFlyweight
fX.Operation(--extrinsicstate);

Flyweight fY = f.GetFlyweight("Y"); //4-把这里的Flyweight换成ConcreteFlyweight
fX.Operation(--extrinsicstate);

Flyweight fZ = f.GetFlyweight("Z"); //5-把这里的Flyweight换成ConcreteFlyweight
fX.Operation(--extrinsicstate); //把以上5处换了之后,结果是一样的,WHY??????

}
}
  回复  引用  查看    

讲得太好了,谢谢吕老师
  回复  引用    

#22楼  2006-10-11 13:18 apsong [未注册用户]
虽然挺简单的东西,
但愿意认真得讲本身就需要耐心;而能讲的初学者明白就是实力了。

故顶。
  回复  引用    

#23楼  2007-03-26 10:12 heqing      
复合享元模式是不是少用呀?也对,学其思想罢了.如果模式也复杂化了....
简单就是美吧.
  回复  引用