标题
Flyweight模式实例 linghushaonian(翻译)
关键字
Flyweight,模式,工厂
出处
Flyweight模式
Flyweight模式用来避免相似类的大量重复。在编程时存在这样一种情形:你似乎需要产生大量的,很小的类的实例来展现数据。但在某些情况下,如果你能意识到其实许多的实例除了少量参数外,本质上是相同的,你将能极大的减少需要实例化的不同类——如果你能将这些变量移到类的实例之外,作为方法的一部分将他们传过来,实例的个数就可以通过共享而极大的减少。
Flyweight设计模式就是提供了处理这种类的一种方法。Flyweight就是指体现实例唯一性的内部数据以及作为参数传进来的外部数据。Flyweight模式适用于小而精的类,像屏幕上单个的字符或是图标。例如,你在windows的屏幕上绘制了一系列的图标,每一个代表了一个人或是数据文件的文件夹,见下图:
在这种情况下,每个文件夹拥有一个类的实例以记住人名等信息以及图标的屏幕位置并不是一个好的方法。在通常情况下,这些图标都是一个很小的图片,而绘出它们的位置是基于window的大小动态计算出来的。
在设计模式的另外一个例子中,文档中的每个字符都是一个字符类的实例,但是他们在屏幕上的位置被视为外部数据。在这种情况下每个字符只需拥有一个实例,而不是每次字符的展现都需要一个。
Flyweight是类的实例共享。
首先看起来似乎每个类都像是个singleton,但实际上可能有少量而非一个的实例,比如一个类可能既有所有字符的一个实例,也有所有图标的一个实例(也比如下面将要谈到的文件夹类,将拥有选中和未选中文件夹这样两个实例)。需要分配的实例的数量必须是确实需要的类实例的数量,而这些工作通常由FlyweightFactory类来完成。这个工厂类通常是个singleton,因为它需要跟踪某个特定的实例是否被产生。它返回一个新的实例,如果实例已经存在则返回一个引用。
为了决定你的部分程序是否适用于Flyweight,你应考虑是否可能从类中移出一些数据作为外部数据。如果这样可以极大的减少需要维护的不同类实例的数量,那么这就是一个Flyweight适用的地方。
实例
假设我们想为组织的每一个人绘制小的文件夹图标,下面标有人名。如果这是个大的组织,那么就可能要大量的这种图标,然而实际上所有的图标都是相同的图片。即使我们需要两个图标——一个表明选中,一个表明未选中——不同图标的数量也是很小的。在这样的系统中,每个人对应一个图标对象,带有姓名,选中状态等信息,是对资源的极大浪费。
因此,现在我们建立一个FolderFactory来返回选中或未选中的文件夹类,而不是建立多余的实例。由于这是一个简单的例子,我们仅在开始就将他们建立并返回他们中的一个。
public class FolderFactory {
private Folder selFolder, unselFolder;
//-----
public FolderFactory() {
//create the two folders
selFolder = new Folder(Color.Brown);
unselFolder = new Folder(Color.Bisque);
}
//-----
public Folder getFolder(bool selected) {
if(selected)
return selFolder;
else
return unselFolder;
}
}
如果有多个实例存在的情况下,工厂应该有一张表来记录哪些已经被创建,只有表中不存在的才能被创建。
我们使用Flyweight来做的唯一事情就是在需要绘制文件夹的时候将姓名等信息传递给它。姓名等是外部数据,因此我们能够共享文件夹对象,在这种情况下,只用建立两个实例。以下完成的文件夹类仅简单建立了一个文件夹实例,拥有两个背景颜色,以及一个public的绘制方法用来在你所指定的地方绘制文件夹。
public class Folder {
//Draws a folder at the specified coordinates
private const int w = 50;
private const int h = 30;
private Pen blackPen, whitePen;
private Pen grayPen;
private SolidBrush backBrush, blackBrush;
private Font fnt;
//------
public Folder(Color col) {
backBrush = new SolidBrush(col);
blackBrush = new SolidBrush(Color.Black);
blackPen = new Pen(Color.Black);
whitePen = new Pen(Color.White);
grayPen = new Pen(Color.Gray);
fnt = new Font("Arial", 12);
}
//-----
public void draw(Graphics g, int x, int y, string title) {
//color folder
g.FillRectangle(backBrush, x, y, w, h);
//outline in black
g.DrawRectangle(blackPen, x, y, w, h);
//left 2 sides have white line
g.DrawLine(whitePen, x + 1, y + 1, x + w - 1, y + 1);
g.DrawLine(whitePen, x + 1, y, x + 1, y + h);
//draw tab
g.DrawRectangle(blackPen, x + 5, y - 5, 15, 5);
g.FillRectangle(backBrush, x + 6, y - 4, 13, 6);
//gray line on right and bottom
g.DrawLine(grayPen, x, y + h - 1, x + w, y + h - 1);
g.DrawLine(grayPen, x + w - 1, y, x + w - 1,
y + h - 1);
g.DrawString(title, fnt, blackBrush, x, y + h + 5);
}
}
要使用一个这样的Flyweight类,你的主程序必须计算出每个文件夹的位置来作为Flyweight的绘制程序的一部分,然后将其它信息传递给文件夹实例。由于你可能根据屏幕的维度需要一个不同的布局,因此你并不希望在你每次绘制的时候都告诉实例新的位置,转而,我们在绘制程序中动态的计算它。
按照上面的设计,我们将在开始就建立一个文件夹列表数组,并简单的扫描该数组以绘制每一个文件夹。这样的数组不同于我们开篇谈到的“大量相似类的实例”,它不是一种浪费,因为它实际上只是对两个文件夹实例的一组引用。由于我们想将文件夹显示为“已选择”,并且可根据选择动态改变该状态,我们将使用文件夹工厂在每个时候自动返回适当的实例。
在我们的显示程序中有两处需要计算文件夹的位置:当绘制它的时候和鼠标悬浮在上面的时候。因此,将位置代码抽象为一个Positioner类将会比较方便。
public class Positioner {
private const int pLeft = 30;
private const int pTop = 30;
private const int HSpace = 70;
private const int VSpace = 80;
private const int rowMax = 2;
private int x, y, cnt;
//-----
public Positioner() {
reset();
}
//-----
public void reset() {
x = pLeft;
y = pTop;
cnt = 0;
}
//-----
public int nextX() {
return x;
}
//-----
public void incr() {
cnt++;
if (cnt > rowMax) { //reset to start new row
cnt = 0;
x = pLeft;
y += VSpace;
}
else {
x += HSpace;
}
}
//-----
public int nextY() {
return y;
}
}
因此我们就能写出更加简单的绘图程序:
private void picPaint(object sender, PaintEventArgs e ) {
Graphics g = e.Graphics;
posn.reset ();
for(int i = 0; i < names.Count; i++) {
fol = folFact.getFolder(selectedName.Equals(
(string)names[i]));
fol.draw(g, posn.nextX() , posn.nextY (),
(string)names[i]);
posn.incr();
}
}
类图
下图展示了这些类之间的交互
(待续)