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