享元模式(Flyweight Pattern)

设计模式 - 吕震宇

.NET设计模式系列文章

薛敬明的专栏

乐在其中设计模式(C#)


设计模式(14)-Flyweight Pattern

一、 享元(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卷:公共语言运行库》,中国电力出版社


.NET设计模式(13):享元模式(Flyweight Pattern)

享元模式(Flyweight Pattern

——.NET设计模式系列之十三

Terrylee,2006年3月

摘要:面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

本文试图通过一个简单的字符处理的例子,运用重构的手段,一步步带你走进Flyweight模式,在这个过程中我们一同思考、探索、权衡,通过比较而得出好的实现方式,而不是给你最终的一个完美解决方案。

主要内容:

1.  Flyweight模式解说

2..NET中的Flyweight模式

3.Flyweight模式的实现要点

……

概述

面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

意图

运用共享技术有效地支持大量细粒度的对象。[GOF 《设计模式》]

结构图

图1  Flyweight模式结构图

生活中的例子

享元模式使用共享技术有效地支持大量细粒度的对象。公共交换电话网(PSTN)是享元的一个例子。有一些资源例如拨号音发生器、振铃发生器和拨号接收器是必须由所有用户共享的。当一个用户拿起听筒打电话时,他不需要知道使用了多少资源。对于用户而言所有的事情就是有拨号音,拨打号码,拨通电话。

图2  使用拨号音发生器例子的享元模式对象图

Flyweight模式解说

Flyweight在拳击比赛中指最轻量级,即“蝇量级”,这里翻译为“享元”,可以理解为共享元对象(细粒度对象)的意思。提到Flyweight模式都会一般都会用编辑器例子来说明,这里也不例外,但我会尝试着通过重构来看待Flyweight模式。考虑这样一个字处理软件,它需要处理的对象可能有单个的字符,由字符组成的段落以及整篇文档,根据面向对象的设计思想和Composite模式,不管是字符还是段落,文档都应该作为单个的对象去看待,这里只考虑单个的字符,不考虑段落及文档等对象,于是可以很容易的得到下面的结构图:

图3

示意性实现代码:

//  "Charactor"
public   abstract   class  Charactor
{
    
//Fields
    protected char _symbol;

    
protected int _width;

    
protected int _height;

    
protected int _ascent;

    
protected int _descent;

    
protected int _pointSize;

    
//Method
    public abstract void Display();
}


//  "CharactorA"
public   class  CharactorA : Charactor

    
// Constructor 
    public CharactorA()
    
{
      
this._symbol = 'A';
      
this._height = 100;
      
this._width = 120;
      
this._ascent = 70;
      
this._descent = 0;
      
this._pointSize = 12;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


//  "CharactorB"
public   class  CharactorB : Charactor
{
    
// Constructor 
    public CharactorB()
    
{
        
this._symbol = 'B';
        
this._height = 100;
        
this._width = 140;
        
this._ascent = 72;
        
this._descent = 0;
        
this._pointSize = 10;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


//  "CharactorC"
public   class  CharactorC : Charactor
{
    
// Constructor 
    public CharactorC()
    
{
        
this._symbol = 'C';
        
this._height = 100;
        
this._width = 160;
        
this._ascent = 74;
        
this._descent = 0;
        
this._pointSize = 14;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


好了,现在看到的这段代码可以说是很好地符合了面向对象的思想,但是同时我们也为此付出了沉重的代价,那就是性能上的开销,可以想象,在一篇文档中,字符的数量远不止几百个这么简单,可能上千上万,内存中就同时存在了上千上万个Charactor对象,这样的内存开销是可想而知的。进一步分析可以发现,虽然我们需要的Charactor实例非常多,这些实例之间只不过是状态不同而已,也就是说这些实例的状态数量是很少的。所以我们并不需要这么多的独立的Charactor实例,而只需要为每一种Charactor状态创建一个实例,让整个字符处理软件共享这些实例就可以了。看这样一幅示意图:

图4

现在我们看到的A,B,C三个字符是共享的,也就是说如果文档中任何地方需要这三个字符,只需要使用共享的这三个实例就可以了。然而我们发现单纯的这样共享也是有问题的。虽然文档中的用到了很多的A字符,虽然字符的symbol等是相同的,它可以共享;但是它们的pointSize却是不相同的,即字符在文档中中的大小是不相同的,这个状态不可以共享。为解决这个问题,首先我们将不可共享的状态从类里面剔除出去,即去掉pointSize这个状态(只是暂时的J),类结构图如下所示:

图5

示意性实现代码:

//  "Charactor"
public   abstract   class  Charactor
{
    
//Fields
    protected char _symbol;

    
protected int _width;

    
protected int _height;

    
protected int _ascent;

    
protected int _descent;

    
//Method
    public abstract void Display();
}


//  "CharactorA"
public   class  CharactorA : Charactor
{
    
// Constructor 
    public CharactorA()
    
{
        
this._symbol = 'A';
        
this._height = 100;
        
this._width = 120;
        
this._ascent = 70;
        
this._descent = 0;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


//  "CharactorB"
public   class  CharactorB : Charactor
{
    
// Constructor 
    public CharactorB()
    
{
        
this._symbol = 'B';
        
this._height = 100;
        
this._width = 140;
        
this._ascent = 72;
        
this._descent = 0;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


//  "CharactorC"
public   class  CharactorC : Charactor
{
    
// Constructor 
    public CharactorC()
    
{
        
this._symbol = 'C';
        
this._height = 100;
        
this._width = 160;
        
this._ascent = 74;
        
this._descent = 0;
    }


    
//Method
    public override void Display()
    
{
        Console.WriteLine(
this._symbol);
    }

}


好,现在类里面剩下的状态都可以共享了,下面我们要做的工作就是控制Charactor类的创建过程,即如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例。如果把这项工作交给Charactor类,即Charactor类在负责它自身职责的同时也要负责管理Charactor实例的管理工作,这在一定程度上有可能违背类的单一职责原则,因此,需要一个单独的类来做这项工作,引入CharactorFactory类,结构图如下:

图6

示意性实现代码:

//  "CharactorFactory"
public   class  CharactorFactory
{
    
// Fields
    private Hashtable charactors = new Hashtable();

    
// Constructor 
    public CharactorFactory()
    
{
        charactors.Add(
"A"new CharactorA());
        charactors.Add(
"B"new CharactorB());
        charactors.Add(
"C"new CharactorC());
    }

       
    
// Method
    public Charactor GetCharactor(string key)
    
{
        Charactor charactor 
= charactors[key] as Charactor;

        
if (charactor == null)
        
{
            
switch (key)
            
{
                
case "A": charactor = new CharactorA(); break;
                
case "B": charactor = new CharactorB(); break
                
case "C": charactor = new CharactorC(); break;
                
//
            }

            charactors.Add(key, charactor);
        }

        
return charactor;
    }

}


到这里已经完全解决了可以共享的状态(这里很丑陋的一个地方是出现了switch语句,但这可以通过别的办法消除,为了简单期间我们先保持这种写法)。下面的工作就是处理刚才被我们剔除出去的那些不可共享的状态,因为虽然将那些状态移除了,但是Charactor对象仍然需要这些状态,被我们剥离后这些对象根本就无法工作,所以需要将这些状态外部化。首先会想到一种比较简单的解决方案就是对于不能共享的那些状态,不需要去在Charactor类中设置,而直接在客户程序代码中进行设置,类结构图如下:

图7

示意性实现代码:

public   class  Program
{
    
public static void Main()
    
{
        Charactor ca 
= new CharactorA();
        Charactor cb 
= new CharactorB();
        Charactor cc 
= new CharactorC();

        
//显示字符

        
//设置字符的大小ChangeSize();
    }


    
public void ChangeSize()
    
{
        
//在这里设置字符的大小
    }

}


按照这样的实现思路,可以发现如果有多个客户端程序使用的话,会出现大量的重复性的逻辑,用重构的术语来说是出现了代码的坏味道,不利于代码的复用和维护;另外把这些状态和行为移到客户程序里面破坏了封装性的原则。再次转变我们的实现思路,可以确定的是这些状态仍然属于Charactor对象,所以它还是应该出现在Charactor类中,对于不同的状态可以采取在客户程序中通过参数化的方式传入。类结构图如下:

图8

示意性实现代码:

//  "Charactor"
public   abstract   class  Charactor
{
    
//Fields
    protected char _symbol;

    
protected int _width;

    
protected int _height;

    
protected int _ascent;

    
protected int _descent;

    
protected int _pointSize;

    
//Method
    public abstract void SetPointSize(int size);
    
public abstract void Display();
}


//  "CharactorA"
public   class  CharactorA : Charactor
{
    
// Constructor 
    public CharactorA()
    
{
        
this._symbol = 'A';
        
this._height = 100;
        
this._width = 120;
        
this._ascent = 70;
        
this._descent = 0;
    }


    
//Method
    public override void SetPointSize(int size)
    
{
        
this._pointSize = size;
    }


    
public override void Display()
    
{
        Console.WriteLine(
this._symbol +
          
"pointsize:" + this._pointSize);
    }

}


//  "CharactorB"
public   class  CharactorB : Charactor
{
    
// Constructor 
    public CharactorB()
    
{
        
this._symbol = 'B';
        
this._height = 100;
        
this._width = 140;
        
this._ascent = 72;
        
this._descent = 0;
    }


    
//Method
    public override void SetPointSize(int size)
    
{
        
this._pointSize = size;
    }


    
public override void Display()
    
{
        Console.WriteLine(
this._symbol +
          
"pointsize:" + this._pointSize);
    }

}


//  "CharactorC"
public   class  CharactorC : Charactor
{
    
// Constructor 
    public CharactorC()
    
{
        
this._symbol = 'C';
        
this._height = 100;
        
this._width = 160;
        
this._ascent = 74;
        
this._descent = 0;
    }


    
//Method
    public override void SetPointSize(int size)
    
{
        
this._pointSize = size;
    }


    
public override void Display()
    
{
        Console.WriteLine(
this._symbol +
          
"pointsize:" + this._pointSize);
    }

}


//  "CharactorFactory"
public   class  CharactorFactory
{
    
// Fields
    private Hashtable charactors = new Hashtable();

    
// Constructor 
    public CharactorFactory()
    
{
        charactors.Add(
"A"new CharactorA());
        charactors.Add(
"B"new CharactorB());
        charactors.Add(
"C"new CharactorC());
    }

       
    
// Method
    public Charactor GetCharactor(string key)
    
{
        Charactor charactor 
= charactors[key] as Charactor;

        
if (charactor == null)
        
{
            
switch (key)
            
{
                
case "A": charactor = new CharactorA(); break;
                
case "B": charactor = new CharactorB(); break
                
case "C": charactor = new CharactorC(); break;
                
//
            }

            charactors.Add(key, charactor);
        }

        
return charactor;
    }

}


public   class  Program
{
    
public static void Main()
    
{
        CharactorFactory factory 
= new CharactorFactory();

        
// Charactor "A"
        CharactorA ca = (CharactorA)factory.GetCharactor("A");
        ca.SetPointSize(
12);
        ca.Display();
        
        
// Charactor "B"
        CharactorB cb = (CharactorB)factory.GetCharactor("B");
        ca.SetPointSize(
10);
        ca.Display();

        
// Charactor "C"
        CharactorC cc = (CharactorC)factory.GetCharactor("C");
        ca.SetPointSize(
14);
        ca.Display();
    }

}


可以看到这样的实现明显优于第一种实现思路。好了,到这里我们就到到了通过Flyweight模式实现了优化资源的这样一个目的。在这个过程中,还有如下几点需要说明:

1.引入CharactorFactory是个关键,在这里创建对象已经不是new一个Charactor对象那么简单,而必须用工厂方法封装起来。

2.在这个例子中把Charactor对象作为Flyweight对象是否准确值的考虑,这里只是为了说明Flyweight模式,至于在实际应用中,哪些对象需要作为Flyweight对象是要经过很好的计算得知,而绝不是凭空臆想。

3.区分内外部状态很重要,这是享元对象能做到享元的关键所在。

到这里,其实我们的讨论还没有结束。有人可能会提出如下问题,享元对象(Charactor)在这个系统中相对于每一个内部状态而言它是唯一的,这跟单件模式有什么区别呢?这个问题已经很好回答了,那就是单件类是不能直接被实例化的,而享元类是可以被实例化的。事实上在这里面真正被设计为单件的应该是享元工厂(不是享元)类,因为如果创建很多个享元工厂的实例,那我们所做的一切努力都是白费的,并没有减少对象的个数。修改后的类结构图如下:

图9

示意性实现代码:

//  "CharactorFactory"
public   class  CharactorFactory
{
    
// Fields
    private Hashtable charactors = new Hashtable();

    
private CharactorFactory instance;
    
// Constructor 
    private CharactorFactory()
    
{
        charactors.Add(
"A"new CharactorA());
        charactors.Add(
"B"new CharactorB());
        charactors.Add(
"C"new CharactorC());
    }

    
    
// Property
    public CharactorFactory Instance
    
{
        
get 
        
{
            
if (instance != null)
            
{
                instance 
= new CharactorFactory();
            }

            
return instance;
        }

    }


    
// Method
    public Charactor GetCharactor(string key)
    
{
        Charactor charactor 
= charactors[key] as Charactor;

        
if (charactor == null)
        
{
            
switch (key)
            
{
                
case "A": charactor = new CharactorA(); break;
                
case "B": charactor = new CharactorB(); break
                
case "C": charactor = new CharactorC(); break;
                
//
            }

            charactors.Add(key, charactor);
        }

        
return charactor;
    }

}


.NET框架中的Flyweight

Flyweight更多时候的时候一种底层的设计模式,在我们的实际应用程序中使用的并不是很多。在.NET中的String类型其实就是运用了Flyweight模式。可以想象,如果每次执行string s1 = “abcd”操作,都创建一个新的字符串对象的话,内存的开销会很大。所以.NET中如果第一次创建了这样的一个字符串对象s1,下次再创建相同的字符串s2时只是把它的引用指向“abcd”,这样就实现了“abcd”在内存中的共享。可以通过下面一个简单的程序来演示s1和s2的引用是否一致:

public   class  Program
{
    
public static void Main(string[] args)
    
{
        
string s1 = "abcd";
        
string s2 = "abcd";

        Console.WriteLine(Object.ReferenceEquals(s1,s2));

        Console.ReadLine();
    }

}


可以看到,输出的结果为True。但是大家要注意的是如果再有一个字符串s3,它的初始值为“ab”,再对它进行操作s3 = s3 + “cd”,这时虽然s1和s3的值相同,但是它们的引用是不同的。关于String的详细情况大家可以参考SDK,这里不再讨论了。

效果及实现要点

1.面向对象很好的解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight设计模式主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。

2.Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。

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

适用性

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

1、   一个系统有大量的对象。

2、   这些对象耗费大量的内存。

3、   这些对象的状态中的大部分都可以外部化。

4、   这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。

5、   软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。

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

总结

Flyweight模式解决的是由于大量的细粒度对象所造成的内存开销的问题,它在实际的开发中并不常用,但是作为底层的提升性能的一种手段却很有效。

参考资料

Erich Gamma等,《设计模式:可复用面向对象软件的基础》,机械工业出版社

Robert C.Martin,《敏捷软件开发:原则、模式与实践》,清华大学出版社

阎宏,《Java与模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast 《C#面向对象设计模式纵横谈(12):Flyweight享元模式(结构型模式)》

http://www.dofactory.com/


乐在其中设计模式(C#) - 享元模式(Flyweight Pattern)

乐在其中设计模式(C#) - 享元模式(Flyweight Pattern)


作者: webabcd


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


示例
有一个Message实体类,某些对象对它的操作有Insert()和Get()方法,现在要运用共享技术支持这些对象。



MessageModel
using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  Pattern.Flyweight
{
    
/// <summary>
    
/// Message实体类
    
/// </summary>

    public class MessageModel
    
{
        
/// <summary>
        
/// 构造函数
        
/// </summary>
        
/// <param name="msg">Message内容</param>
        
/// <param name="pt">Message发布时间</param>

        public MessageModel(string msg, DateTime pt)
        
{
            
this._message = msg;
            
this._publishTime = pt;
        }


        
private string _message;
        
/// <summary>
        
/// Message内容
        
/// </summary>

        public string Message
        
{
            
get return _message; }
            
set { _message = value; }
        }


        
private DateTime _publishTime;
        
/// <summary>
        
/// Message发布时间
        
/// </summary>

        public DateTime PublishTime
        
{
            
get return _publishTime; }
            
set { _publishTime = value; }
        }

    }

}


AbstractMessage
using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  Pattern.Flyweight
{
    
/// <summary>
    
/// 操作Message抽象类(Flyweight)
    
/// </summary>

    public abstract class AbstractMessage
    
{
        
/// <summary>
        
/// 获取Message
        
/// </summary>
        
/// <returns></returns>

        public abstract List<MessageModel> Get();

        
/// <summary>
        
/// 插入Message
        
/// </summary>
        
/// <param name="mm">Message实体对象</param>
        
/// <returns></returns>

        public abstract bool Insert(MessageModel mm);
    }

}


SqlMessage
using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  Pattern.Flyweight
{
    
/// <summary>
    
/// Sql方式操作Message(ConcreteFlyweight)
    
/// </summary>

    public class SqlMessage : AbstractMessage
    
{
        
/// <summary>
        
/// 获取Message
        
/// </summary>
        
/// <returns></returns>

        public override List<MessageModel> Get()
        
{
            List
<MessageModel> l = new List<MessageModel>();
            l.Add(
new MessageModel("SQL方式获取Message", DateTime.Now));

            
return l;
        }


        
/// <summary>
        
/// 插入Message
        
/// </summary>
        
/// <param name="mm">Message实体对象</param>
        
/// <returns></returns>

        public override bool Insert(MessageModel mm)
        
{
            
// 代码略
            return true;
        }

    }

}


XmlMessage
using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  Pattern.Flyweight
{
    
/// <summary>
    
/// Xml方式操作Message(ConcreteFlyweight)
    
/// </summary>

    public class XmlMessage : AbstractMessage
    
{
        
/// <summary>
        
/// 获取Message
        
/// </summary>
        
/// <returns></returns>

        public override List<MessageModel> Get()
        
{
            List
<MessageModel> l = new List<MessageModel>();
            l.Add(
new MessageModel("XML方式获取Message", DateTime.Now));

            
return l;
        }


        
/// <summary>
        
/// 插入Message
        
/// </summary>
        
/// <param name="mm">Message实体对象</param>
        
/// <returns></returns>

        public override bool Insert(MessageModel mm)
        
{
            
// 代码略
            return true;
        }

    }

}


MessageFactory
using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  Pattern.Flyweight
{
    
/// <summary>
    
/// Message工厂(FlyweightFactory)
    
/// </summary>

    public class MessageFactory
    
{
        
private Dictionary<string, AbstractMessage> _messageObjects = new Dictionary<string, AbstractMessage>();

        
/// <summary>
        
/// 获取Message对象
        
/// </summary>
        
/// <param name="key">key</param>
        
/// <returns></returns>

        public AbstractMessage GetMessageObject(string key)
        
{
            AbstractMessage messageObject 
= null;

            
if (_messageObjects.ContainsKey(key))
            
{
                messageObject 
= _messageObjects[key];
            }

            
else
            
{
                
switch (key)
                
{
                    
case "xml": messageObject = new SqlMessage(); break;
                    
case "sql": messageObject = new XmlMessage(); break;
                }


                _messageObjects.Add(key, messageObject);
            }


            
return messageObject;
        }

    }

}



Test
using  System;
using  System.Data;
using  System.Configuration;
using  System.Collections;
using  System.Web;
using  System.Web.Security;
using  System.Web.UI;
using  System.Web.UI.WebControls;
using  System.Web.UI.WebControls.WebParts;
using  System.Web.UI.HtmlControls;

using  Pattern.Flyweight;

public  partial  class  Flyweight : System.Web.UI.Page
{
    
protected void Page_Load(object sender, EventArgs e)
    
{
        
string[] ary = new string[] "xml""sql" };

        MessageFactory messageFactory 
= new MessageFactory();

        
foreach (string key in ary)
        
{
            AbstractMessage messageObject 
= messageFactory.GetMessageObject(key);

            Response.Write(messageObject.Insert(
new MessageModel("插入", DateTime.Now)));
            Response.Write(
"<br />");
            Response.Write(messageObject.Get()[
0].Message + " " + messageObject.Get()[0].PublishTime.ToString());
            Response.Write(
"<br />");
        }

    }

}


运行结果
True
SQL方式获取Message 2007-5-17 22:20:38
True
XML方式获取Message 2007-5-17 22:20:38


参考
http://www.dofactory.com/Patterns/PatternFlyweight.aspx


OK
[源码下载]



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值