享元模式

网盘存储同一份文件被不同用户反复保存该怎么办?

意图

运用共享技术有效地支持大量细粒度的对象

动机

GoF 中提到的例子就很形象,一个文档编辑器中,尽管用对象来表示文档中的 每个字符 会极大地提高应用程序的灵活性,但是即使是一个中等大小的文档也可能要求成百上千的字符对象,这会耗费大量内存,产生难以接受的运行开销。所以通常并不是对每个字符都用一个对象来表示的。 享元(Flyweight)模式 描述了如何共享对象,使得可以细粒度地使用他们而无需高昂的代价

这里的解决方案就是,我们预先将所有的字符创建一遍,形成一个字符池,文档在生成对象的时候只需要从字符池中获取对象,不额外创建重复的对象,另外客户端额外保存字符的位置比如几行第几个字符是什么,排版风格是什么等信息

GoF 在这里引入2个概念 内部状态外部状态

  • 内部状态:内部状态存储于 flyweight 中,它包含了独立于 flyweight 场景的信息,这些信息使得 flyweight 可以被共享
  • 外部状态:外部状态取决于 Flyweight 场景,并根据场景而变化,因此不可共享,用户对象负责在必要的时候将外部状态传递给 Flyweight

进一步分析文档编辑器这个例子,理解一下这2个概念的意思

文档编辑器中可以为字母表中每一个 字母 创建一个 flyweight, 每个 flyweight 存储一个字符代码,但它在文档中的位置和排版风格可以在字符出现时由正文排版算法和使用的格式化命令决定,其中字符代码是内部状态,而其他的信息则是外部状态,把这些状态参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目

UML图

uml

  • Flyweight
    描述一个接口,通过这个接口 flyweight 可以接受并作用于外部状态
  • ConcreteFlyweight
    实现 Flyweight 接口,并为内部状态 (如果有的话) 增加存储空间。 ConcreteFlyweight 对象必须是共享的。它所存储的状态必须是内部的
  • UnsharedConcreteFlyweight
    并非所有的 Flyweight 子类都需要被共享,Flyweight 接口使共享成为可能,但它并不强制共享。
  • FlyweightFactory
    创建并管理 flyweight 对象; 确保合理地共享 flyweight。当用户请求一个 flyweight 时,FlyweightFactory 对象提供一个已创建的实例或者创建一个(如果不存在的话)。
  • Client
    维持一个对 flyweight 的引用; 计算或存储一个(多个) flyweight 的外部状态

注意:

  • flyweight 执行时所需的状态必定是内部的或外部的。内部状态存储于 ConcreteFlyweight 对象中;而外部状态则由 Clinet 对象存储或计算。当用户调用 flyweight 对象的操作时,将该状态传递给它。
  • 用户不应直接对 ConcreteFlyweight 类进行实例化,而只能从 FlyweightFactory 对象得到 ConcreteFlyweight 对象,这可以保证对它们适当地进行共享。

应用场景

  • 一个应用程序使用了大量的对象
  • 完全由于使用大量的对象,造成很大的存储开销
  • 对象的大多数状态都可变成外部状态
  • 如果删除对象的外部装,那么可以用相对较少的共享对象取代很多组对象

举个例子

以围棋为例,简单按照共享模式的思路,创建一下棋子,以及棋子的移动

先定义棋子,棋子最核心的部分就是

  • 棋子的颜色
  • 被棋手放到指定位置

先定义一个 GoPieces 这个抽象基类

class GoPieces
{
public:
    GoPieces(string color) : m_color(color) {}
    virtual ~GoPieces() {}
    virtual void moveTo(string person, int x, int y) = 0;

private:
    string m_color;
};

其次是2个黑白棋子的具体类

class WhiteGoPieces : public GoPieces
{
public:
    WhiteGoPieces(): GoPieces("white") {}

    virtual void moveTo(string person, int x, int y){
        cout << person << " 将白色棋子放置在: (" << x << ","<< y<< ")"<< endl;
    }
};

class BlackGoPieces : public GoPieces
{
public:
    BlackGoPieces(): GoPieces("black") {}

    virtual void moveTo(string person, int x, int y){
        cout << person << " 将黑色棋子放置在: (" << x << ","<< y<< ")"<< endl;
    }
};

根据享元模式的思路,创建 GoPiecesFactory, 主要就是负责获取具体棋子对象

class GoPiecesFactory
{
public:
    ~GoPiecesFactory() {
        for(auto iter = m_goPieces.begin(); iter != m_goPieces.end();iter++)
        {
            auto piece = iter->second;
            SAFE_DELETE(piece);
        }
        m_goPieces.clear();
    }

    GoPieces* GetGoPieces(string color) {
        auto it = m_goPieces.find(color);
        if (it != m_goPieces.end()) {
            return it->second;
        } else {
            GoPieces* temp = nullptr;
            if (color == "black")
                temp = new BlackGoPieces();
            else
                temp = new WhiteGoPieces();

            m_goPieces.insert({color,temp});
            return temp;
        }
    }

private:
    map<string, GoPieces*> m_goPieces;
};

最后看一下 main 函数部分

#include "gopieces.h"

int main()
{
    GoPiecesFactory *factory = new GoPiecesFactory;

    string personA = "A";
    string personB = "B";

    factory->GetGoPieces("black")->moveTo(personA, 1, 1);
    factory->GetGoPieces("white")->moveTo(personB, 2, 2);
    factory->GetGoPieces("black")->moveTo(personA, 1, 2);
    factory->GetGoPieces("white")->moveTo(personB, 2, 1);

    SAFE_DELETE(factory);

    return 0;
}

运行结果

A 将黑色棋子放置在: (1,1)
B 将白色棋子放置在: (2,2)
A 将黑色棋子放置在: (1,2)
B 将白色棋子放置在: (2,1)

其实这里的 GoPiecesFactory 可以进一步设计成单例的形式,感觉会更好一点

这里有个需要注意的点, GoPiecesFactory 管理的棋子对象的生命周期, 通过 GetGoPieces 获取到的棋子对象不能被客户端方面直接 delete

总结

整理完享元模式,最大的感觉就是:

运用共享技术将大量细粒度的对象进项复用,并且提供一个 享元池 ,最后通过享元工厂类将享元对象提供给客户端使用,核心其实就是 享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。