【Design Pattern】Structural - Flyweight/ Object Pool

Posted by 西维蜀黍 on 2018-11-11, Last Modified on 2022-12-10

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 - 调用者调用代码

每次点击DrawButton时,都会(随机指定位置、长度、高度和颜色)绘制一个随机图案:

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对象。

在多次点击DrawButton后,就会出现这样的结果:

6 优缺点

优点

  • 大幅度地降低了内存中对象是数量

缺点

  • 使得系统更加复杂(为了使对象可以共享,需要将一些状态外蕴化,这客观上导致系统的逻辑复杂了)

7 应用实例

最典型的对享元模式的应用,是Java中对String的字符串常量池(String Constant Pool)的实现。

类似地,Java中包装类Integer、Byte、Boolean、Character等也采用了享元模式。

此外,享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。

Reference