一、概述
当类的部分属性在整个系统中的多个对象间重复出现时,一个通常的作法是将重复出现的属性从类定义中分离出来,并在多个对象间通过共享来节约系统开销,这种情况在界面相关的应用中尤其常见。如用于浏览目录内容的树,每个节点前面有一个Icon用于表示该节点的类型,如果将该Icon保存在每个节点的数据结构中,无疑是一种巨大的浪费,这时候通过共享(每个节点只需要保存一个所使用Icon的标识即可,在C ++中,可以通过引用、指针或ID标识等来实现)可以提高性能,并且当被共享的次数越多时,这种提高就越明显。
Flyweight(享元)模式采用共享来避免大量拥有相同内容对象的开销,这种开销中最常见、最直观的就是内存的损耗,Flyweight模式以共享的方式高效地支持大量的细粒度对象。
二、结构
存在两种典型的运用Flyweight模式的情形:单纯Flyweight模式和复合Flyweight模式。
单纯Flyweight模式的类图结构如下:
图 1:单纯Flyweight模式类图示意
在上面的类图中包括以下组成部分:
1、抽象享元 (Flyweight )角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口,通过这个接口Flyweight可以接受并作用于外部状态(Extrinsic State)。
2、具体享元 (ConcreteFlyweight )角色:实现抽象享元角色所规定的接口。如果有内蕴状态(Intrinsic State)的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
3、享元工厂 (FlyweightFactory )角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
4、客户端 (Client )角色:本角色需要维护一个对所有享元对象的引用。本角色需要自行存储所有享元对象的外蕴状态。
单纯Flyweight模式在收到对象创建请求时检查是否该类型对象已存在,若存在,则直接返回该对象,否则,创建新的对象。单纯Flyweight模式的的结构十分简单,其思想与Singleton模式及Simple Factory Pattern也有几分相似之处,但单纯Flyweight模式注重对多个对象(数量不确定)的共享,希望通过这种共享来达到效率或者空间上的节省,而Singleton模式注重对对象创建数目的控制,Simple Factory Pattern则注重对对象创建细节的屏蔽和分离。
复合Flyweight模式的类图结构如下:
图 2:复合Flyweight模式类图示意
在上面的类图中包括以下组成部分:
1 )抽象享元角色:为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。
2 )具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。
3 )复合享元角色:它所代表的对象是不可以共享的,并且可以分解成为多个单纯享元对象的组合。
4 )享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!
5 )客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。
复合Flyweight模式通过组合的方式来结合关联的具有相同Extrinsic属性而Intrinsic属性不同的多个单纯Flyweight对象,是Flyweight模式的一种延伸。
三、应用
在以下情况下可以考虑使用Flyweight模式:
1 )系统中有大量的对象,他们使系统的效率降低。
2 )这些对象的状态可以分离出所需要的内外两部分。
GoF的DP一书举出了一个字处理的例子,由于在通常情况下,一篇文档中字符及其字体颜色属性的组合并不会太多(如果是纯文本文件,我认为没有使用Flyweight模式的必要,因为存一个字符跟存一个字符的索引的消耗是相当的),根据GoF的统计,一篇包含 180 , 000个字符(英文)的文档需要分配的Flyweight的数目大约只有 480个。因此,通过保存各字符的索引(通过字符的颜色、大小等信息进行分类,可以对保存的策略进行进一步优化,如DP一书提到采用B -Tree进行存储),而不是实际保存每一个字符以及其大小、颜色信息,可以大大节约实际使用的内存大小。但是,话说回来,虽然我没有实际测试过,但是,个人认为这种存储策略可能在很多情况下并非最优,对于类似的情况,其它一些处理策略,如采用类似位图行程压缩的方式存放属性变化信息,而将文档内容以纯文本形式存放,在很多情况下可能空间使用效率也非常高,只是可能需要涉及比较复杂的逻辑处理。
我不知道是由于Flyweight模式的名字的原因或者其它什么原因,在通常所能看到的关于Flyweight模式的材料中总是假设被共享的对象很小,我并不同意这种观点。实际上,个人认为,Flyweight模式对于大的对象(可能内存消耗大,也可能创建成本高)更有价值,如连接池 /线程池就是共享大的对象的最好的例证,只是由于大的对象往往具有更多的属性,这在一定程度上阻碍了共享的发生。
四、优缺点
享元模式优点就在于它能够大幅度的降低内存中对象的数量;而为了做到这一步也带来了它的缺点:使得系统逻辑变得更加复杂,而且在一定程度上外蕴状态影响了系统的速度。
同时,外蕴状态和内蕴状态的划分,以及两者关系的对应关系也是必须考虑的因素。只有将内外划分妥当才能使内蕴状态发挥它应有的作用;如果划分失误,可能在空间和时间两个方面都得不偿失。
五、举例
上面已经说过,准确划分Intrinsic State和Extrinsic State是应用Flyweight模式的关键,划分时应保证内蕴状态尽可能多,而外蕴状态尽可能少,以充分利用共享减小重复消耗。
作为一个设计良好的程序库,在JDK中存在着一些运用Flyweight模式的例子,如BorderFactory就是一个享元工厂类,下面的例子输出为Yes:
import javax .swing .*;
import javax .swing .border .*;
public class BorderTest {
public static void main (String [] args ) {
BorderTest test = new BorderTest ();
}
public BorderTest () {
Border border1 = BorderFactory .createRaisedBevelBorder ();
Border border2 = BorderFactory .createRaisedBevelBorder ();
if (border1 == border2 )
System .out .println ( "Yes. Two borders are shared" );
else
System .out .println ( "No. Two borders are NOT shared" );
}
}
此外,Java中的String和JTree、JTable等也通过共享使用部分公共元素,使得性能得以提升。
下面举一个绘图的例子,通常来讲,图形会包含线型、线宽、颜色等信息,在一个包含大量线条(直线或曲线)的绘图系统中,如果在每一个线条对象中均保存这些信息,无疑是一种巨大的浪费。为此,我们将线条对象的属性进行如下划分:
Intrinsic State:
Color
LineWidth
...
Extrinsic State ( for line ):
Start Point
End Point
根据以上划分,相应示例的实现如下(Java Code):
import java .awt .*;
import java .awt .event .*;
import javax .swing .*;
import javax .swing .event .*;
import java .util .ArrayList ;
public class FlyweightTest extends JFrame {
private static final Color colors [] = { Color .red , Color .blue ,
Color .yellow , Color .orange ,
Color .black , Color .white };
private static final int WINDOW_WIDTH = 400 , WINDOW_HEIGHT = 400 , NUMBER_OF_LINES = 100 ;
private ArrayList vLine = new ArrayList ();
JButton button = new JButton ( "draw lines" );
final JPanel panel = new JPanel ();
public static void main (String [] args ) {
FlyweightTest test = new FlyweightTest ();
test .show ();
}
public FlyweightTest () {
super ( "Flyweight Test" );
Container contentPane = getContentPane ();
contentPane .setLayout ( new BorderLayout ());
contentPane .add (panel , BorderLayout .CENTER );
contentPane .add (button , BorderLayout .SOUTH );
setBounds ( 20 , 20 , WINDOW_WIDTH , WINDOW_HEIGHT );
setDefaultCloseOperation (JFrame .EXIT_ON_CLOSE );
button .addActionListener ( new ActionListener () {
public void actionPerformed (ActionEvent event ) {
vLine .clear ();
for ( int i = 0 ; i < NUMBER_OF_LINES ; i ++) {
int index = LineFlyweightFactory .getIndex (getRandomColor (), getRandomWidth ());
vLine .add ( new Line ( new Point (getRandomX (), getRandomY ()), new Point (getRandomX (), getRandomY ()), index ));
}
repaint ();
}
});
}
private int getRandomX () {
return ( int )(Math .random () * WINDOW_WIDTH );
}
private int getRandomY () {
return ( int )(Math .random () * WINDOW_HEIGHT );
}
private Color getRandomColor () {
return colors [( int )(Math .random () * colors .length )];
}
private int getRandomWidth () {
return ( int )(Math .random () * 5 );
}
public void paint (Graphics g ) {
super .paint (g );
Graphics gp = panel .getGraphics ();
Line line ;
for ( int i = 0 ; i < vLine .size (); i ++) {
line = (Line )vLine .get (i );
line .draw (gp );
}
}
}
// class which contains extrinsic state and reference to flyweight
class Line {
private Point start , end ;
private int index ; // reference to flyweight
public Line (Point start , Point end , int index ) {
this .start = start ;
this .end = end ;
this .index = index ;
}
public void draw (Graphics g ) {
LineFlyweight line = LineFlyweightFactory .getLine (index );
line .draw (g , start .x , start .y , end .x , end .y ); // pass extrinsic state to flyweight
}
}
// Flyweight
class LineFlyweight {
// intrinsic state
private Color color ;
private BasicStroke stroke ;
public LineFlyweight (Color color , float lineWidth ) {
this .color = color ;
stroke = new BasicStroke (lineWidth );
}
public boolean equals (Color color , int lineWidth ) {
if ( this .color .equals (color ) && (stroke .getLineWidth () == lineWidth ))
return true ;
return false ;
}
public void draw (Graphics g , int x , int y , int x2 , int y2 ) {
Graphics2D g2 = (Graphics2D )g ;
g2 .setColor (color );
g2 .setStroke (stroke );
g2 .drawLine (x , y , x2 , y2 );
}
}
// Flywight Factory
class LineFlyweightFactory {
private static final ArrayList vFlyweight = new ArrayList ();
public static int getIndex (Color color , int lineWidth ) {
LineFlyweight line ;
for ( int i = 0 ; i < vFlyweight .size (); i ++) {
line = (LineFlyweight )vFlyweight .get (i );
if (line .equals (color , lineWidth ))
return i ;
}
line = new LineFlyweight (color , lineWidth );
vFlyweight .add (line );
System .out .println ( "Creating " + color + " line with width = " + lineWidth );
return vFlyweight .size () - 1 ;
}
public static LineFlyweight getLine ( int index ) {
if (index > vFlyweight .size ())
return null ;
return (LineFlyweight )vFlyweight .get (index );
}
}
当类的部分属性在整个系统中的多个对象间重复出现时,一个通常的作法是将重复出现的属性从类定义中分离出来,并在多个对象间通过共享来节约系统开销,这种情况在界面相关的应用中尤其常见。如用于浏览目录内容的树,每个节点前面有一个Icon用于表示该节点的类型,如果将该Icon保存在每个节点的数据结构中,无疑是一种巨大的浪费,这时候通过共享(每个节点只需要保存一个所使用Icon的标识即可,在C ++中,可以通过引用、指针或ID标识等来实现)可以提高性能,并且当被共享的次数越多时,这种提高就越明显。
Flyweight(享元)模式采用共享来避免大量拥有相同内容对象的开销,这种开销中最常见、最直观的就是内存的损耗,Flyweight模式以共享的方式高效地支持大量的细粒度对象。
二、结构
存在两种典型的运用Flyweight模式的情形:单纯Flyweight模式和复合Flyweight模式。
单纯Flyweight模式的类图结构如下:
图 1:单纯Flyweight模式类图示意
在上面的类图中包括以下组成部分:
1、抽象享元 (Flyweight )角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口,通过这个接口Flyweight可以接受并作用于外部状态(Extrinsic State)。
2、具体享元 (ConcreteFlyweight )角色:实现抽象享元角色所规定的接口。如果有内蕴状态(Intrinsic State)的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
3、享元工厂 (FlyweightFactory )角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
4、客户端 (Client )角色:本角色需要维护一个对所有享元对象的引用。本角色需要自行存储所有享元对象的外蕴状态。
单纯Flyweight模式在收到对象创建请求时检查是否该类型对象已存在,若存在,则直接返回该对象,否则,创建新的对象。单纯Flyweight模式的的结构十分简单,其思想与Singleton模式及Simple Factory Pattern也有几分相似之处,但单纯Flyweight模式注重对多个对象(数量不确定)的共享,希望通过这种共享来达到效率或者空间上的节省,而Singleton模式注重对对象创建数目的控制,Simple Factory Pattern则注重对对象创建细节的屏蔽和分离。
复合Flyweight模式的类图结构如下:
图 2:复合Flyweight模式类图示意
在上面的类图中包括以下组成部分:
1 )抽象享元角色:为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。
2 )具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。
3 )复合享元角色:它所代表的对象是不可以共享的,并且可以分解成为多个单纯享元对象的组合。
4 )享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!
5 )客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。
复合Flyweight模式通过组合的方式来结合关联的具有相同Extrinsic属性而Intrinsic属性不同的多个单纯Flyweight对象,是Flyweight模式的一种延伸。
三、应用
在以下情况下可以考虑使用Flyweight模式:
1 )系统中有大量的对象,他们使系统的效率降低。
2 )这些对象的状态可以分离出所需要的内外两部分。
GoF的DP一书举出了一个字处理的例子,由于在通常情况下,一篇文档中字符及其字体颜色属性的组合并不会太多(如果是纯文本文件,我认为没有使用Flyweight模式的必要,因为存一个字符跟存一个字符的索引的消耗是相当的),根据GoF的统计,一篇包含 180 , 000个字符(英文)的文档需要分配的Flyweight的数目大约只有 480个。因此,通过保存各字符的索引(通过字符的颜色、大小等信息进行分类,可以对保存的策略进行进一步优化,如DP一书提到采用B -Tree进行存储),而不是实际保存每一个字符以及其大小、颜色信息,可以大大节约实际使用的内存大小。但是,话说回来,虽然我没有实际测试过,但是,个人认为这种存储策略可能在很多情况下并非最优,对于类似的情况,其它一些处理策略,如采用类似位图行程压缩的方式存放属性变化信息,而将文档内容以纯文本形式存放,在很多情况下可能空间使用效率也非常高,只是可能需要涉及比较复杂的逻辑处理。
我不知道是由于Flyweight模式的名字的原因或者其它什么原因,在通常所能看到的关于Flyweight模式的材料中总是假设被共享的对象很小,我并不同意这种观点。实际上,个人认为,Flyweight模式对于大的对象(可能内存消耗大,也可能创建成本高)更有价值,如连接池 /线程池就是共享大的对象的最好的例证,只是由于大的对象往往具有更多的属性,这在一定程度上阻碍了共享的发生。
四、优缺点
享元模式优点就在于它能够大幅度的降低内存中对象的数量;而为了做到这一步也带来了它的缺点:使得系统逻辑变得更加复杂,而且在一定程度上外蕴状态影响了系统的速度。
同时,外蕴状态和内蕴状态的划分,以及两者关系的对应关系也是必须考虑的因素。只有将内外划分妥当才能使内蕴状态发挥它应有的作用;如果划分失误,可能在空间和时间两个方面都得不偿失。
五、举例
上面已经说过,准确划分Intrinsic State和Extrinsic State是应用Flyweight模式的关键,划分时应保证内蕴状态尽可能多,而外蕴状态尽可能少,以充分利用共享减小重复消耗。
作为一个设计良好的程序库,在JDK中存在着一些运用Flyweight模式的例子,如BorderFactory就是一个享元工厂类,下面的例子输出为Yes:
import javax .swing .*;
import javax .swing .border .*;
public class BorderTest {
public static void main (String [] args ) {
BorderTest test = new BorderTest ();
}
public BorderTest () {
Border border1 = BorderFactory .createRaisedBevelBorder ();
Border border2 = BorderFactory .createRaisedBevelBorder ();
if (border1 == border2 )
System .out .println ( "Yes. Two borders are shared" );
else
System .out .println ( "No. Two borders are NOT shared" );
}
}
此外,Java中的String和JTree、JTable等也通过共享使用部分公共元素,使得性能得以提升。
下面举一个绘图的例子,通常来讲,图形会包含线型、线宽、颜色等信息,在一个包含大量线条(直线或曲线)的绘图系统中,如果在每一个线条对象中均保存这些信息,无疑是一种巨大的浪费。为此,我们将线条对象的属性进行如下划分:
Intrinsic State:
Color
LineWidth
...
Extrinsic State ( for line ):
Start Point
End Point
根据以上划分,相应示例的实现如下(Java Code):
import java .awt .*;
import java .awt .event .*;
import javax .swing .*;
import javax .swing .event .*;
import java .util .ArrayList ;
public class FlyweightTest extends JFrame {
private static final Color colors [] = { Color .red , Color .blue ,
Color .yellow , Color .orange ,
Color .black , Color .white };
private static final int WINDOW_WIDTH = 400 , WINDOW_HEIGHT = 400 , NUMBER_OF_LINES = 100 ;
private ArrayList vLine = new ArrayList ();
JButton button = new JButton ( "draw lines" );
final JPanel panel = new JPanel ();
public static void main (String [] args ) {
FlyweightTest test = new FlyweightTest ();
test .show ();
}
public FlyweightTest () {
super ( "Flyweight Test" );
Container contentPane = getContentPane ();
contentPane .setLayout ( new BorderLayout ());
contentPane .add (panel , BorderLayout .CENTER );
contentPane .add (button , BorderLayout .SOUTH );
setBounds ( 20 , 20 , WINDOW_WIDTH , WINDOW_HEIGHT );
setDefaultCloseOperation (JFrame .EXIT_ON_CLOSE );
button .addActionListener ( new ActionListener () {
public void actionPerformed (ActionEvent event ) {
vLine .clear ();
for ( int i = 0 ; i < NUMBER_OF_LINES ; i ++) {
int index = LineFlyweightFactory .getIndex (getRandomColor (), getRandomWidth ());
vLine .add ( new Line ( new Point (getRandomX (), getRandomY ()), new Point (getRandomX (), getRandomY ()), index ));
}
repaint ();
}
});
}
private int getRandomX () {
return ( int )(Math .random () * WINDOW_WIDTH );
}
private int getRandomY () {
return ( int )(Math .random () * WINDOW_HEIGHT );
}
private Color getRandomColor () {
return colors [( int )(Math .random () * colors .length )];
}
private int getRandomWidth () {
return ( int )(Math .random () * 5 );
}
public void paint (Graphics g ) {
super .paint (g );
Graphics gp = panel .getGraphics ();
Line line ;
for ( int i = 0 ; i < vLine .size (); i ++) {
line = (Line )vLine .get (i );
line .draw (gp );
}
}
}
// class which contains extrinsic state and reference to flyweight
class Line {
private Point start , end ;
private int index ; // reference to flyweight
public Line (Point start , Point end , int index ) {
this .start = start ;
this .end = end ;
this .index = index ;
}
public void draw (Graphics g ) {
LineFlyweight line = LineFlyweightFactory .getLine (index );
line .draw (g , start .x , start .y , end .x , end .y ); // pass extrinsic state to flyweight
}
}
// Flyweight
class LineFlyweight {
// intrinsic state
private Color color ;
private BasicStroke stroke ;
public LineFlyweight (Color color , float lineWidth ) {
this .color = color ;
stroke = new BasicStroke (lineWidth );
}
public boolean equals (Color color , int lineWidth ) {
if ( this .color .equals (color ) && (stroke .getLineWidth () == lineWidth ))
return true ;
return false ;
}
public void draw (Graphics g , int x , int y , int x2 , int y2 ) {
Graphics2D g2 = (Graphics2D )g ;
g2 .setColor (color );
g2 .setStroke (stroke );
g2 .drawLine (x , y , x2 , y2 );
}
}
// Flywight Factory
class LineFlyweightFactory {
private static final ArrayList vFlyweight = new ArrayList ();
public static int getIndex (Color color , int lineWidth ) {
LineFlyweight line ;
for ( int i = 0 ; i < vFlyweight .size (); i ++) {
line = (LineFlyweight )vFlyweight .get (i );
if (line .equals (color , lineWidth ))
return i ;
}
line = new LineFlyweight (color , lineWidth );
vFlyweight .add (line );
System .out .println ( "Creating " + color + " line with width = " + lineWidth );
return vFlyweight .size () - 1 ;
}
public static LineFlyweight getLine ( int index ) {
if (index > vFlyweight .size ())
return null ;
return (LineFlyweight )vFlyweight .get (index );
}
}