跳转至

享元模式(Flyweight)

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

个人理解

实体类的类的属性可以分为内部状态和外部状态。内部状态是享元对象所共享的状态,外部状态是享元对象所独有的状态。享元模式通过将内部状态提取出来,减少了内存的使用。享元类指的就是存储内部状态的类,类中的属性状态通常是不可变的。 这些 内部状态会被存储在一个容器中,外部通过传入内部状态信息去容器中获取指定的享元对象,如果容器中没有,则会通过传入的内部状态信息创建一个新的享元对象,并存储到容器中。 这样就可以保证这些对象是可以共用的,不会重复创建。

一个例子:现在需要使用画笔绘制东西,一般会创建一个绘制操作类,包含了绘制的画笔和绘制的东西,但这样会导致每次都会创建新的画笔。由于画笔的样式(粗细、颜色等)是可以复用的,因此我们可以将画笔的样式提取出来,做为一个内部状态类放进一个容器中,绘制的东西做为外部状态类(场景类)。每次绘制时,先通过画笔的样式信息从容器中获取到画笔对象,如果容器中没有,则创建一个新的画笔对象并存储到容器中。这样就可以减少内存的使用。

享元模式结构

享元模式结构图

  1. 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。

  2. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。

  3. 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。

  4. 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。

  5. 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。

  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

实现方式

  1. 将需要改写为享元的类成员变量拆分为两个部分:

    - 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。 - 外在状态: 包含每个对象各自不同的情景数据的成员变量 2. 保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。 3. 找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。 4. 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。 5. 客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。

优缺点

优点 缺点
如果程序中有很多相似对象, 那么你将可以节省大量内存。 你可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。
代码会变得更加复杂。 团队中的新成员总是会问:“为什么要像这样拆分一个实体的状态?”。

伪代码

    // 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一
    // 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜
    // 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗
    // 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象
    // 中,然后让众多的单个树对象去引用它。
    class TreeType is
        field name
        field color
        field texture
        constructor TreeType(name, color, texture) { …… }
        method draw(canvas, x, y) is
            // 1. 创建特定类型、颜色和纹理的位图。
            // 2. 在画布坐标 (X,Y) 处绘制位图。

    // 享元工厂决定是否复用已有享元或者创建一个新的对象。
    class TreeFactory is
        static field treeTypes: collection of tree types
        static method getTreeType(name, color, texture) is
            type = treeTypes.find(name, color, texture)
            if (type == null)
                type = new TreeType(name, color, texture)
                treeTypes.add(type)
            return type

    // 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们
    // 体积很小:仅有两个整型坐标和一个引用成员变量。
    class Tree is
        field x,y
        field type: TreeType
        constructor Tree(x, y, type) { …… }
        method draw(canvas) is
            type.draw(canvas, this.x, this.y)

    // 树(Tree)和森林(Forest)类是享元的客户端。如果不打算继续对树类进行开
    // 发,你可以将它们合并。
    class Forest is
        field trees: collection of Trees

        method plantTree(x, y, name, color, texture) is
            type = TreeFactory.getTreeType(name, color, texture)
            tree = new Tree(x, y, type)
            trees.add(tree)

        method draw(canvas) is
            foreach (tree in trees) do
                tree.draw(canvas)