设计模式(11) 享元模式(FLYWEIGHT)

问题关注:
    如果让我们设计一款文字编辑器,面向对象的设计方式肯定是首选。
    一种可能的设计方式是,将文档中的每个字符,每行,每列都认为是一个对象,提高程序的灵活性的同时,可以想象,将会耗费大量内存。
    享元模式,就是为了应对这种情况——共享对象,细粒度处理而无需高昂的代价。


意图:
    运用共享技术,有效地支持大量细粒度的对象。

动机:
    面向对象技术极大地提高了软件开发过程中的每个环节的效率,但是如果过于简单地使用它,代价也是极大的。
     以在引言里提到的文档编辑器的设计为例。
     常规设计:用对象来表示文档中的每个字符,每一行,每一列
     优点:极大提高应用程序的灵活性。
     缺点:极高的内存花费和运行开销。
    如图:
    
     下面将用享元模式来优化这个编辑器的设计:
     设计目标:细粒度地处理对象而无需高昂的代价。
     途径:共享对象
     关键概念:flyweight是一个共享对象,可以同时在多个场景(context)中使用,并且在每个场景中flyweight都可以作为一个独立的对象。flyweight不能对它所运行的场景做出任何假设。使用flyweight需要区分内部状态和外部状态。内部状态存储于flyweight中,它包含了独立于flyweight场景的信息,这些信息使得flyweight可以被共享;外部状态取决于flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给flyweight。(一直习惯于一句话长度的总结,不过这段还是原样展示比较好理解。)
     场景特点:Flyweight模式对那些通常因为数量太大而难以用对象来表示的概念或实体进行建模。
     设计方式:
  • 为字母表中的每个字母创建一个flyweight,每个flyweight存储一个字符代码,但它在文档中的位置和排版风格可以在字符出现时由正文排版算法和使用的格式化命令决定。因此,字符代码是内部状态,而其他的信息则是外部状态。
  • 逻辑上,文档中的所有字符每次出现都有一个对象与其对应。
  • 物理上,字母表中的每个字符共享一个flyweight,所以同一个字符出现时,指向同一个实例,这个实例位于flyweight对象的共享池中。
  • 如下图所示的那样,表示字母的flyweight只存储相应的字符代码,并不关心位置和字体等一些外部信息。
  • 必须要的外部信息,由row对象,在绘制时,提供给字幕flyweight。
    如下图所示:
     逻辑上:
    
     物理上:
    
     结构:
    


适用性:
Flyweight模式的使用场景要求比较严格。
其有效性很大程度上取决于如何使用它以及在何处使用它。
当一下情况“都成立”时可以使用Flyweight模式:
  • 一个应用程序使用了大量的对象
  • 完全由于使用大量的对性爱那个,造成很大的存储开销
  •  对象的大多数状态都可变为外部状态
  • 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象
  • 应用程序不依赖于对象标识。由于Flyweight对象可以被共享,对于概念上明显有别的镀锡i昂,标识测试将返回真值
结构:
    
    下面的对象图说明了如何共享flyweight
    


参与者:
  • Flyweight(Glyph):描述了一个接口,通过这个接口flyweight可以接受并作用于外部状态
  • ConcreteFlyweight(Character):实现Flyweight接口,并为内部状态增加存储空间
  • UnsharedConcreteFlyweight(Row, Column):并非所有的Flyweight子类都需要被共享。在Flyweight对象结构的某些层次中,UnsharedConcreteFlyweight对象通常将ConreteFlyweight作为字节带你,提供给它们外部信息。
  • FlyweightFactory:创建并管理flyweight对象;确保合理地共享flyweight,当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建爱你一个(如果不存在的话)。
协作:
  • flyweight执行时,所需要的状态必定是内部的或外部的。
  • 用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证它们适当地进行共享。
效果:
节省内存,因素如下:
  • 共享,使得实例总数减少
  • 对象内部状态的平均数目减少
  • 外部状态是计算得到时更提高效率,当外部状态主要是计算出来的时,节约量打到最大
所以,Flyweight模式用两种方法节省内存:
  • 用共享减少内部状态的消耗
  • 用计算时间换取对外部状态的存储
实现:
实现Flyweight模式时,注意一下几点:
  • 处理外部状态
    •     如果不同种类的外部状态和共享前对象的数目相同的话,删除外部状态不会降低存储消耗
    •     理想情况,外部状态由一个单独的对象计算得到,且该结构的存储要求非常小
    •     在文档编辑器例子中,可以用一个单独的结构存储排版布局信息,而不是存储每一个字符对象的字体和类型信息
  • 管理共享对象
    •     用户不可以对共享对象进行直接实例化,因此FlyweightFactory可以帮助用户查找某个特定的Flyweight对象。


代码示例:
以上文提到的文档编辑器为例,重点讨论字体属性。
类设定:
Glyph类:为Flyweight的图形对象定义的基类,属于Composite类(见 组合模式Composite),有图形化属性,并可以绘制自己。
class Glyph {
public:
    virtual ~Glyph();
    virtual void Draw(Window*, GlyphContext&);

    virtual void SetFont(Font*, GlyphContext&);
    virtual Font* GetFont(GlyphContext&);
    virtual void First(GlyphContext&);
    virtual void Next(GlyphContext&);
    virtual bool IsDone(GlyphContext&);
    virtual Glyph* Current(GlyphContext&);

    virtual void Insert(Glyph*, GlyphContext&);
    virtual void Remove(GlyphContext&);
protected:
    Glyph();
};
Character的子类存储一个字符代码:
class Character : public Glyph {
public:
    Character(char);
    virtual void Draw(Window*, GlyphContext&);
private:
    char _charcode;
};
GlyphContext
     作用:避免给每个Glyph的字体属性都分配空间,可以将该属性外部存储于GlyphContext对象中
     实现:GlyphContext是一个外部状态的存储库,它维持Glyph与字体之间的一种简单映射关系。
     使用:对于任何操作,如果它需要知道在给定场景下Glyph字体,都会有一个GlyphContext实例作为参数传递给它。然后该操作就可以查询GlyphContext以获取该场景中的字体信息了。
     注意:当使用Glyph时,Glyph子类的迭代和管理操作必须更新GlyphContext。
class Glyph {
public:
    GlyphContext();
    virtual ~GlyphContext();
    
    virtual void Next(int step = 1);
    virtual void Insert(int quantity = 1);
    virtual Font* GetFont();
    virtual void SetFont(Font*, int span = 1);
private:
    int _index;
    BTree* _fonts;
};
Btree:  存储Glyph到字体的映射。树中的每个节点都标有字符串的长度,而向这个字符串传递字体信息。树中的叶节点指向一种字体,而内部的字符串分成了很多子字符串,每一个对应一种叶节点。
     文本信息如下:
    
     Btree结构可能如下图所示:
    
     假设遍历到索引102。
     目标1:将“except”的字体设置为“Time 12”字体。
    代码如下:
GlyphContext gc;
Font* time12 = new Font("Times-Roman-12");
Font* timeItalic12 = new Font("Times-Italic-12");
// ...
gc.SetFont(times12, 6);
     新的Btree结构如下所示(黑体表示变动):
    
     目标2:在单词“expect”前用12-point Times Italic字体添加一个单词Don't(包含一个紧跟着的空格)。假定gc仍在索引位置102
    代码:
gc.Insert(6);
gc.SetFont(timesItalic12, 6);
     变动后的Btree结构:
    
    使用Btree结构的优点:
  •     查询当前Glyph字体时,向下搜索Btree,同时增加索引,直至找到当前索引的字体为止
  •     由于字体变化频率相对较低,所以这棵树相对于Glyph结构较小
  •     存储耗费低,同时也不会过多的增加查询时间
FlyweightFactory类:负责创建Glyph类,并确保它们进行合理共享,即Glyph类的管理类。
    职责:实例化Character和其他类型的Glyph。
const int NCHARCODES = 128;

class GlyphFactory {
public:
    GlyphFactory();
    virtual ~GlyphFactory();
    virtual Character* CreateCharacter(char);
    virtual Row* CreateRow();
    virtual Column* CreateColumn();
    // ...
private:
    Character* _character[NCHARCODES];
};

// _character数组包含一些指针,指向以字母代码为索引的Character Glyphs。该数组在构造函数中被初始化为零。
GlyphFactory::GlyphFactory () {
    for (int i = 0; i < NCHARCODES; ++i) {
        _character[i] = 0;    
    }
};

//对某个字符的查找,如果存在,返回相应的Glyph,如果不存在,创建相应的Glyph,将其放入数组中
Character* GlyphFactory::CreateCharacter (char c) {
    if (!_character[c]) {
        _character[c] = new Character(c);
    }
    return _character[c];
}

// 其他操作仅需要在每次被调用时实例化一个新对象,因为非字符的Glyph不能被共享
Row* GlyphFactory::CreateRow () {
    return new Row;
}

Column* GlyphFactory::CreateColumn () {
    return new Column;
}


相关模式:
  • Flyweight模式通常和组合模式结合起来,用共享叶节点的有向无环图实现一个逻辑上的层次结构
  • 通常,最好用Flyweight实现State和Strategy对象

参考资料:
《设计模式:可复用面向对象软件的基础》


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值