文章

23种设计模式-享元模式

Flyweight模式 享元模式

Intent/目的

使用共享来有效地支持大量的细粒度对象。

Motivation/动机

一些应用程序可以通过在其设计中贯穿使用对象来受益,但是一个简单的实现会代价昂贵到令人望而却步。

例如,大多数文档编辑器实现都在某种程度上模块化了文本格式化和编辑功能。面向对象的文档编辑器通常使用对象来代表嵌入的元素,如表格和图形。然而,他们通常不会为文档中的每一个字符使用一个对象,尽管这样做可以在应用程序的最细层次上促进灵活性。这样,字符和嵌入的元素就可以在它们如何被绘制和格式化方面被统一处理。应用程序可以扩展以支持新的字符集,而不影响其他功能。应用程序的对象结构可以模仿文档的物理结构。以下图表展示了一个文档编辑器如何使用对象来代表字符。

这种设计的缺点是它的成本。即使是中等大小的文档也可能需要成千上万的字符对象,这将消耗大量内存并可能导致无法接受的运行时开销。享元模式描述了如何共享对象,以允许在不引起过高成本的情况下,以细粒度使用它们。

享元是一种可以同时在多个上下文中使用的共享对象。在每个上下文中,享元表现得就像一个独立的对象——它与未共享的对象实例无法区分。享元不能对它们操作的上下文做出假设。这里的关键概念是内在状态(intrinsic)和外在状态(extrinsic)之间的区别。内在状态存储在享元中;它包括与享元的上下文无关的信息,因此可以共享。外在状态依赖于并随享元的上下文变化,因此不能共享。客户端对象负责在需要时将外在状态传递给享元。

逻辑上,文档中给定字符的每次出现都有一个对象:

然而,在物理上,每个字符只有一个共享的享元对象,并且它在文档结构的不同上下文中出现。特定字符对象的每次出现都引用共享的享元对象池中的同一个实例:

接下来展示的是这些对象的类结构。Glyph 是图形对象的抽象类,其中一些可能是享元。依赖于外在状态的操作将外在状态作为参数传递给它们。例如,DrawIntersects 必须知道 glyph 在哪个上下文中,才能执行它们的工作。

代表字母 “a” 的享元仅存储相应的字符代码;它不需要存储其位置或字体。客户端提供享元绘制自己所需的依赖于上下文的信息。例如, Row glyph 知道其子元素应该在哪里绘制自己以便它们水平排列。因此,它可以在绘制请求中传递给每个子元素其位置。

因为不同字符对象的数量远小于文档中的字符数量,所以总共的对象数量大大少于一个简单实现所使用的数量。在一个所有字符都以相同字体和颜色出现的文档中,无论文档的长度如何,都将分配大约100个字符对象(大致相当于ASCII字符集的大小)。而且,由于大多数文档使用的不同字体-颜色组合不超过10种,因此这个数字在实践中不会显著增长。因此,对于单个字符来说,对象抽象变得切实可行。

Applicability/应用场景

享元模式的有效性在很大程度上取决于它如何以及在哪里被使用。当以下所有条件都满足时,应用享元模式:

  • 应用程序使用了大量的对象。
  • 由于对象数量众多,存储成本很高。
  • 大多数对象状态可以变为外在的。
  • 一旦移除外在状态,许多组对象可以被相对较少的共享对象替代。
  • 应用程序不依赖于对象身份。由于享元对象可能会被共享,身份测试对于概念上不同的对象将返回真(true)。

Structure/结构

Participants/角色

  • Flyweight (Glyph)
    • 声明了一个接口,通过该接口,享元可以接收并对外在状态进行操作。
  • ConcreteFlyweight (Character)
    • 实现了享元接口,并为内在状态(如果有的话)添加了存储空间。一个具体享元(ConcreteFlyweight)对象必须是可共享的。它存储的任何状态都必须是内在的;也就是说,它必须独立于具体享元对象的上下文
  • UnsharedConcreteFlyweight (Row, Column)
    • 并非所有享元子类都需要被共享。享元接口使共享成为可能;它并不强制共享。在享元对象结构中的某个层级,未共享的具体享元(UnsharedConcreteFlyweight)对象有具体享元(ConcreteFlyweight)对象作为其子对象是很常见的(就像Row和Column类所做的那样).
  • FlyweightFactory
    • 创建并管理享元对象
    • 确保享元被正确共享。当客户端请求一个享元时,享元工厂(FlyweightFactory)对象提供一个现有的实例,或者如果不存在任何实例,就创建一个。
  • Client
    • 维护对享元的引用
    • 计算或存储享元的外在状态。

Consequences/总结

享元可能会引入与传输、查找和/或计算外在状态相关的运行时成本,特别是如果这些状态以前作为内在状态存储的话。然而,这些成本被空间节省所抵消,随着共享的享元数量增加,空间节省也会增加。

存储节省是几个因素的函数:

  • 由共享带来的实例总数减少
  • 每个对象的内在状态量
  • 外在状态是被计算还是被存储

共享的享元越多,存储节省越大。随着共享状态量的增加,节省也增加。当对象使用大量的内在状态和外在状态,并且外在状态可以被计算而不是存储时,节省最大。这样你可以通过两种方式节省存储空间:共享减少了内在状态的成本,而你用计算时间来交换外在状态。

享元模式经常与组合模式(Composite pattern)结合使用,以将层次结构表示为具有共享叶节点的图。共享的一个后果是,享元叶节点不能存储指向其父节点的指针。相反,父指针作为其外在状态的一部分传递给享元。这对层次结构中的对象如何相互通信产生了重大影响。

享元模式经常与组合模式结合使用,以实现逻辑层次结构,这种结构以共享叶节点的有向无环图(directed-acyclic graph)的形式出现。

通常最好将状态(State)和策略(Strategy)对象实现为享元。

本文由作者按照 CC BY 4.0 进行授权

© Poul.Y. 保留部分权利。