- 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
- 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
- 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。
设计意图
GoF举例
大部分文档编辑器通常都具有排版和编辑的功能, 这些功能在实现时往往会进行一定程度的模块化。 在使用面向对象的设计方式时, 会很自然地想要用对象来代表文档中内嵌的元素,例如表格和图形。 然而, 编辑器应用通常并不会为文档中的每一个字符使用一个对象, 尽管这样做可以提供如下的灵活性:
之所以不采用这种设计的原因是开销. 如果采用该设计, 即便是中等大小的文档也会有成千上万的字符对象,这会消耗大量的内存且有可能导致无法令人接受的运行时开销。
解决方案(享元模式)
概念说明
概念应用:
享元模式的有效性很大程度上取决于其使用方式和使用场景。 注意, 只有在下列所有条件都成立的情况下应用享元模式:
享元模式作为一个相对较为复杂的模式, 仅仅通过上述的说明并不能完全理解其使用方式, 故需要进一步详细的例子。这里依旧以文档应用为例, 进行实现层面的举例说明。
这里主要关注如何把 字体的 extrinsic state 信息 Font 从 Character 对象中剥离出来, 使得 Character 对象中之存储字符码, 从而减少内存消耗。
Glyph 类是一个复合对象(可以参考设计模式拾荒之组合模式: 复杂的树形结构)
class Glyph { public: virtual ~Glyph(); virtual void draw(Window*, GlyphContext&); virtual void detFont(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 类是 Glyhph 的子类, 仅仅存储字符码 _charcode
class Character : public Glyph {
public:
Character(char);
virtual void draw(Window*, GlyphContext&);
private:
char _charcode;
};
为了避免为每一个 Character 对象的 font 属性分配空间, 我们将 font 这个属性外在地存储在一个 GlyphContext 对象中。 GlyphContext 扮演了 extrinsic state 的一个中央仓库的角色。 它以一种紧凑的方式维护了不同上下文 Character 对象 和其 font 属性间的映射关系。 任何需要在特定上下文获得 Character 对象 font 信息才能执行的方法 , 都会有一个 GlyphContext 对象作为参数传递给这个方法。 然后该方法就可以从 GlyphCotext 这个对象中获得其上下文的 font 信息。GlyphContext 的信息取决于 Character 对象所在上下文的具体位置, 因此对于 Character ( Glyph ) 的子节点的遍历和操作都必须更新 GlyphContext , 无论它是否被使用。
class GlyphContext {
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;
考虑下图的字符组合 , 注意到单词”expect”的索引是 102-107
存储 font 信息的 BTree 结构可能是如下形式
内部节点定义了 character 索引的范围 。 (注意到, 上图中BTree 中的 children 节点中的数值总和即为父节点的数值, 例如 300 = 100+6 +194) BTree 在字体被改变时 和 Character 被从Glyph 对象中添加或移除时 会被更新。 例如, 假设我们遍历到102 号索引, 下列代码将单词 “expect” 中的每个字符的字体设置成了和其周围字符的文本一样的字体 ( 即为 times12 , 一个 12-point 的 Times Roman 的Font 对象)。
GlyphContext gc;
Font* times12 = new Font("Times-Roman-12");
Font* timesItalic12 = new Font("Times-Italic-12");
// ...
gc.setFont(times12, 6); // 方法定义是 SetFont(Font*, int span = 1);
此时记录 Font 信息的 BTree 会被更新为如下形式
当我们在 “expect” 前增加单词 “don’t ”(包含尾部的一个空格) ,其字体为 12-point Times Italic 时。 下列代码把该事件通知给 glyphContext 对象 gc。
gc.insert(6);
gc.setFont(timesItalic12, 6);
当从 GlyphContext 中查询某个Character 对象的字体时, 就可以从Btree 的根节点开始搜索, 直到找到Character 索引所对应的位置, 然后就可以获得该索引所对应的 Character 的 Font 信息。
由于文档中的不同字体的数量通常较少, 且字体变动的频率也比较低, 所以这棵树可以保持的相对较小,查询所需额时间也很难增长到不可接受的程度。
此外,我们还需要一个 FlyWeightFactory 来创建 glyphs 且确保他们被合适地共享。
GlyphFactory 类负责实例化 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 对象的指针 , 在一开始, _character数组中 值会被初始化为 0 ;
GlyphFactory::GlyphFactory () {
for (int i = 0; i < NCHARCODES; ++i) {
_character[i] = 0;
}
}
CreateCharacter 的函数在创建字符的时候会首先检查该字符是否已经被创建过, 如果创建过,则直接返回已创建的对象指针, 没有, 则创建该对象, 并将该对象的索引保存在数组中其ASCII码对应的位置处。
Character* GlyphFactory::CreateCharacter (char c) {
if (!_character[c]) {
_character[c] = new Character(c);
}
return _character[c];
}
由于 Row 和 Column 对象不被共享, 则可以直接创建
Row* GlyphFactory::createRow () {
return new Row;
}
Column* GlyphFactory::createColumn () {
return new Column;
}
上述的代码整体看下来之后,可能还是比较让人困惑, 原因是似乎没有一个地方解释了图例中 500个 Character 的 index(1-500) 维护在哪里。
// Glyph 的 draw 方法是需要传入GlyphContext 对象作为参数的
// virtual void draw(Window*, GlyphContext&);
// next 方法的定义为 virtual void next(GlyphContext&);
while( ! row.isDone )
{
row.next(glyphContext); // 这个方法会把 glyphContext 中的 _index 更新到下一行 row 第一个 Character 所对应的 index 值
row.draw(window, glyphContext);
}
此时需要注意, 由于应用了组合模式(Composite), row.draw()的内部实现是:
// 此处的for 循环采用伪代码写法 , 因为row 中以什么方式维护属于该行的 Character 并不重要, 只要能够遍历即可
for(all c in characters) //
{
c.draw(window, glyphContext ) // 每一次调用都会需要对glyphContext 的 _index 属性作递增操作, 使其指向下一个 character
}
组合模式的结构图回忆: ( ^_^ )
GlyphContext 作为一个 extrinsic state 的中央仓库, 在绘制过程中是需要维护其状态的。