大卫的Design Patterns学习笔记10:Flyweight

一、概述
当类的部分属性在整个系统中的多个对象间重复出现时,一个通常的作法是将重复出现的属性从类定义中分离出来,并在多个对象间通过共享来节约系统开销,这种情况在界面相关的应用中尤其常见。如用于浏览目录内容的树,每个节点前面有一个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 );
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值