1 动机
通过尽可能共享对象中的数据来实现对内存的最小化使用,这就是享元模式的动机。
一个典型的例子是:在Word中,对字符的图形化展现。如果每一个显示的字符都对应一个独立的对象,这将会消耗大量的内存。取而代之,位于文档中不同位置的相同字符会共享大部分属性,而只有字符位置这样的属性才需要被单独保存。
2 定义
享元模式(Flyweight Pattern)通过在享元工厂中引入享元池实现了对对象的多次复用,从而减少了内存的使用。
内蕴状态(intrinsic state)和外蕴状态(extrinsic state)
享元对象能做到共享的关键是区分内蕴状态(intrinsic state)和外蕴状态(extrinsic state)。
内蕴状态(intrinsic state):被共享,因为内蕴状态不会因为环境改变而被改变(invariant),或者说是上下文独立的(context independent)。比如,字符“A”在给定的一个字符集中的编码。
而**外蕴状态(extrinsic state)**会被实例独有,或者说同一类型的不同实例它们的外蕴状态会不同,比如在一个出现多次字符“A”的文档中,各个“A”的位置。
享元模式的广泛使用
享元模式在文本编辑器系统中被大量应用,一个文本编辑器通常会提供多种不同的字体。我们可以将每一个已经使用到的字母做成享元对象(FlyweightConcreteClass):
- 字母本身和字母对应的编码属于一个字母的內蕴状态
- 字母的位置、字体等则属于该字母的外蕴状态
在Java中, String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,Java会确保一个字符串常量在常量池中只有一个拷贝。String a = "abc"
,其中"abc"就是一个字符串常量。
3 适用场景
当存在以下情况时,可以考虑使用享元模式:
- 有大量对象存在的应用
- 存在占用内存较大或创建需耗费大量时间的对象
- 对象的属性可以被区分为固有属性(内蕴状态)和非固有属性(外蕴状态)
需要注意的是,享元模式中中需要维护一个记录了系统已有所有享元对象的表,这同样是需要耗费资源的。因此,只要在有足够多的享元实例可供共享时,才值得使用享元模式。
4 构成
- 抽象享元角色(FlyweightInterface):定义享元类的接口,需要将对外蕴状态的操作(Operation)通过参数的形式传入方法中。
- 具体享元角色(ConcreteFlyweight):实现抽象享元角色所规定的接口。如果有内蕴状态,可以将该内蕴状态存储于类内部。享元对象的内蕴状态与对象所处的上下文环境无关。
- 享元工厂角色(FlyweightFactory):负责创建与管理享元角色。当调用者通过享元工厂获取一个享元对象时,享元工厂需要检查系统是否已经有一个已经存在的享元对象。如果有,则直接提供这个享元对象给调用者。若无,享元工厂则需要先创建一个享元对象(并将具体享元角色的实例存储于**享元池(Flyweight Pool)**中),再提供给调用者。
享元模式的核心在于享元工厂类,享元工厂提供了一个用于存储享元对象的享元池。当调用者需要某个享元对象时,享元工厂会先从享元池中获取,若不在,则会新实例化一个享元对象,并在享元池中增加该对象,并返回给用户。
UML图
抽象享元角色类
public interface Flyweight {
//一个示意性方法,参数state是外蕴状态
public void operation(String state);
}
具体享元角色类
public class ConcreteFlyweight implements Flyweight {
private Character intrinsicState = null;
/**
* 构造函数,内蕴状态作为参数传入
*
* @param state
*/
public ConcreteFlyweight(Character state) {
this.intrinsicState = state;
}
/**
* 外蕴状态作为参数传入方法中,改变方法的行为,
* 但是并不改变对象的内蕴状态。
*/
@Override
public void operation(String state) {
// TODO Auto-generated method stub
System.out.println("Intrinsic State = " + this.intrinsicState);
System.out.println("Extrinsic State = " + state);
}
}
具体享元角色类(ConcreteFlyweight)可以有内蕴状态,内蕴状态的值应当在享元对象被创建时就被赋值。所有的内蕴状态在对象创建之后,就不会再改变了。
如果一个享元对象有外蕴状态的话,所有的外部状态都必须存储在客户端,在使用享元对象时,再由客户端传入享元对象。这里只有一个外蕴状态,operation()方法的参数state就是由外部传入的外蕴状态。
享元工厂角色类
public class FlyweightFactory {
private Map<Character, Flyweight> files = new HashMap<Character, Flyweight>();
public Flyweight factory(Character state) {
//先从缓存中查找对象
Flyweight fly = files.get(state);
if (fly == null) {
//如果对象不存在则创建一个新的Flyweight对象
fly = new ConcreteFlyweight(state);
//把这个新的Flyweight对象添加到缓存中
files.put(state, fly);
}
return fly;
}
}
必须指出的是,客户端不可以直接将具体享元类实例化,而必须通过一个工厂对象,并调用其factory()方法得到享元对象。一般而言,享元工厂对象在整个系统中只有一个,因此也可以使用单例模式。
当客户端需要单纯享元对象的时候,需要调用享元工厂的factory()方法,并传入所需的单纯享元对象的内蕴状态,由工厂方法产生所需要的享元对象。
调用者
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
FlyweightFactory factory = new FlyweightFactory();
Flyweight fly = factory.factory(new Character('a'));
fly.operation("First Call");
fly = factory.factory(new Character('b'));
fly.operation("Second Call");
fly = factory.factory(new Character('a'));
fly.operation("Third Call");
}
}
5 示例
UML图
实现思路
- Shape接口:享元接口,图形接口
- Line类:享元对象
- Oval类:享元对象
- ShapeFactory类:享元工厂,可以向调用者提供享元对象
Shape.java
import java.awt.Color;
import java.awt.Graphics;
public interface Shape {
public void draw(Graphics g, int x, int y, int width, int height,
Color color);
}
Line.java
import java.awt.Color;
import java.awt.Graphics;
public class Line implements Shape {
public Line(){
System.out.println("Creating Line object");
// adding time delay:模拟一个类的实例化需要耗费大量时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics line, int x1, int y1, int x2, int y2,
Color color) {
line.setColor(color);
line.drawLine(x1, y1, x2, y2);
}
}
Oval.java
import java.awt.Color;
import java.awt.Graphics;
public class Oval implements Shape {
//intrinsic property
private boolean fill;
public Oval(boolean f){
this.fill=f;
System.out.println("Creating Oval object with fill="+f);
// adding time delay:模拟一个类的实例化需要耗费大量时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics circle, int x, int y, int width, int height,
Color color) {
circle.setColor(color);
circle.drawOval(x, y, width, height);
if(fill){
circle.fillOval(x, y, width, height);
}
}
}
ShapeFactory.java
public class ShapeFactory {
private static final HashMap<ShapeType,Shape> shapes = new HashMap<ShapeType,Shape>();
public static Shape getShape(ShapeType type) {
Shape shapeImpl = shapes.get(type);
if (shapeImpl == null) {
if (type.equals(ShapeType.OVAL_FILL)) {
shapeImpl = new Oval(true);
} else if (type.equals(ShapeType.OVAL_NOFILL)) {
shapeImpl = new Oval(false);
} else if (type.equals(ShapeType.LINE)) {
shapeImpl = new Line();
}
shapes.put(type, shapeImpl);
}
return shapeImpl;
}
public static enum ShapeType{
OVAL_FILL,OVAL_NOFILL,LINE;
}
}
DrawingClient.java - 调用者调用代码
每次点击Draw
Button时,都会(随机指定位置、长度、高度和颜色)绘制一个随机图案:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class DrawingClient extends JFrame{
private static final long serialVersionUID = -1350200437285282550L;
private final int WIDTH;
private final int HEIGHT;
private static final ShapeType shapes[] = { ShapeType.LINE, ShapeType.OVAL_FILL,ShapeType.OVAL_NOFILL };
private static final Color colors[] = { Color.RED, Color.GREEN, Color.YELLOW };
public DrawingClient(int width, int height){
this.WIDTH=width;
this.HEIGHT=height;
Container contentPane = getContentPane();
JButton startButton = new JButton("Draw");
final JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.CENTER);
contentPane.add(startButton, BorderLayout.SOUTH);
setSize(WIDTH, HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for (int i = 0; i < 20; ++i) {
Shape shape = ShapeFactory.getShape(getRandomShape());
shape.draw(g, getRandomX(), getRandomY(), getRandomWidth(),
getRandomHeight(), getRandomColor());
}
}
});
}
private ShapeType getRandomShape() {
return shapes[(int) (Math.random() * shapes.length)];
}
private int getRandomX() {
return (int) (Math.random() * WIDTH);
}
private int getRandomY() {
return (int) (Math.random() * HEIGHT);
}
private int getRandomWidth() {
return (int) (Math.random() * (WIDTH / 10));
}
private int getRandomHeight() {
return (int) (Math.random() * (HEIGHT / 10));
}
private Color getRandomColor() {
return colors[(int) (Math.random() * colors.length)];
}
public static void main(String[] args) {
DrawingClient drawing = new DrawingClient(500,600);
}
}
结果分析
由于对每个Shape
对象进行初始化是非常耗时的,因此我们采用享元模式以避免多次初始化同一个类型的Shape
对象。
在多次点击Draw
Button后,就会出现这样的结果:
6 优缺点
优点
- 大幅度地降低了内存中对象是数量
缺点
- 使得系统更加复杂(为了使对象可以共享,需要将一些状态外蕴化,这客观上导致系统的逻辑复杂了)
7 应用实例
最典型的对享元模式的应用,是Java中对String的字符串常量池(String Constant Pool)的实现。
类似地,Java中包装类Integer、Byte、Boolean、Character等也采用了享元模式。
此外,享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。
Reference
- 《Java与模式》
- Design Patterns: Elements of Reusable Object-Oriented Software
- JournalDev - Flyweight Design Pattern in Java - https://www.journaldev.com/1562/flyweight-design-pattern-java
- https://github.com/iluwatar/java-design-patterns/tree/master/flyweight