C++ 享元模式

2025年3月25日 | 阅读 13 分钟

引言

Flyweight 模式是“四人帮”所描述的结构性设计模式之一。当您需要高效地支持大量细粒度对象时,可以使用此模式。该模式旨在通过尽可能多地与其他相似对象共享来实现内存使用或计算费用的最小化。在需要创建大量对象并且其大部分状态可以共享的情况下,此模式尤其有用。

Flyweight 模式的组成部分

Flyweight 接口/类: 定义了 flyweight 对象的接口。它通常包含一个用于执行某些操作的方法,该方法可以接受外部状态作为参数。

Concrete Flyweight(具体 Flyweight): 这是 Flyweight 接口的实现。它包含可以被多个对象共享的内部状态。

Flyweight Factory(Flyweight 工厂): 负责创建、存储和共享 flyweight 对象。它确保在请求时重用相同的 flyweight 对象,而不是重新创建。

Client(客户端): 使用 Flyweight 对象。必要时,它可能将外部状态传递给 flyweight 对象。

工作原理

内部状态与外部状态的分离: 在 Flyweight 模式中,对象被分为内部状态和外部状态。内部状态是通用的,可以被多个对象共享;而外部状态是可变的,不能被共享。

共享通用状态: Flyweight 工厂确保在请求 flyweight 对象时,会检查是否已存在具有相同内部状态的对象。如果存在,则返回现有对象;如果不存在,则创建一个新对象。

减少内存使用: 通过共享通用状态,Flyweight 模式减少了存储对象所需的内存量,尤其是在有许多相似对象实例的情况下。

提高性能: Flyweight 模式可以通过减少需要管理的对象的数量和内存开销来提高应用程序的性能。

考虑一个文本编辑器应用程序,其中一个对象代表文档中的每个字符。我们可以使用 Flyweight 模式,而不是为每个字符创建单独的对象。

内部状态: 内部状态将包括字体、大小颜色等属性,这些属性对于相同类型的字符是恒定的。

外部状态: 外部状态将包括位置和样式等属性,这些属性对于每个字符都不同。

方法一:基础 Flyweight 实现

在基础的 Flyweight 实现中,我们首先创建一个 Flyweight 接口,用于定义 flyweight 对象的各种操作。Concrete Flyweight 实现此接口,表示具有共享内部状态的实际 flyweight 对象。Flyweight Factory 管理 flyweights,确保它们在可能的情况下被共享和重用。它根据给定的键创建 flyweights,并维护现有 flyweights 的集合。

当客户端请求 flyweight 时,工厂要么返回一个现有的 flyweight,要么在 flyweight 不存在时创建一个新的 flyweight。这避免了重复的对象创建,最大限度地减少了内存使用。Flyweight 模式通过分离共享状态和唯一状态,实现了对大量细粒对象的有效管理。

程序

输出

Character: A Size: 12 Color: Black
Character: B Size: 10 Color: Red
Character: A Size: 12 Color: Black
free(): double free detected in cache 2
Aborted

说明

提供的代码实现了 Flyweight 模式,这是一种结构性设计模式,旨在高效管理大量细粒对象。当应用程序需要处理大量具有相似属性的对象以减少内存使用和提高性能时,此模式特别有用。

在此实现中,我们有三个主要组成部分:Flyweight 接口、Concrete Flyweight 和 Flyweight Factory。

  • Flyweight 接口
    Character 接口充当所有 flyweight 对象的基础。它定义了一个通用的 print() 操作,该操作由所有具体的 flyweights 实现。此接口允许我们统一地处理不同的 flyweight 对象。
  • Concrete Flyweight
    ConcreteCharacter 类代表具体的 flyweight 对象。它封装了字符的内部状态,包括字符的符号、大小和颜色。该类负责提供每种字符类型的特定行为。在我们的示例中,字符“A”和“B”被表示为具体的 flyweights。
  • Flyweight Factory
    CharacterFactory 类充当创建和管理 flyweight 对象的工厂。它确保 flyweights 在可能的情况下被共享和重用,从而减少了创建的对象数量并最大限度地减少了内存使用。该工厂维护一个 flyweight 对象集合,并以字符符号作为键进行高效检索。在请求时,如果不存在,它将返回一个现有的 flyweight 对象,否则会创建一个新的对象。
  • 主函数
    在 main() 函数中,我们演示了 Flyweight 模式的用法。我们创建了 CharacterFactory 的实例,并使用它获取代表字符“A”和“B”的 flyweight 对象。然后,我们使用 print() 方法打印它们的属性。最后,我们演示了请求另一个“A”字符会重用现有的 flyweight 对象,从而最大限度地减少了内存开销。

Flyweight 模式通过在相似对象之间共享通用属性来优化内存使用。我们创建 flyweight 对象来表示多个字符共享的通用属性,而不是为每个字符创建单独的对象。这种共享减少了应用程序的总内存占用,尤其是在处理大量细粒对象时。

处理文本、图形元素或游戏对象的应用程序可以从 Flyweight 模式中受益匪浅。例如,在文本编辑器中,文档中的每个字符都由一个对象表示,使用 Flyweight 模式可以显著减少内存消耗并提高性能。

Flyweight 模式通过共享对象的通用状态,提供了一种有效管理资源的方法。因此,它在处理大量相似对象的应用程序中优化了内存使用并提高了性能。

复杂度分析

Flyweight 模式实现的时间和空间复杂度取决于各种因素,例如创建的flyweight 对象的数量、内部状态的大小以及所使用的底层数据结构的效率。

时间复杂度

Flyweight 模式实现的时间和空间复杂度可能取决于工厂数据结构的效率以及创建的 flyweight 对象的大小和数量。通过一个设计良好的、使用高效数据结构的工厂,创建和访问 flyweight 对象的时间复杂度可以保持在O(1)

创建 Flyweight 对象

创建 flyweight 对象时,时间复杂度主要取决于用于在工厂中存储和检索 flyweight 对象的的数据结构的效率。如果使用哈希映射或类似的数据结构来实现常数时间查找,则创建 flyweight 对象的时间复杂度为O(1)

但是,如果使用线性搜索或其他效率较低的方法,则时间复杂度可能为O(n),其中 n 是已创建的 flyweight 对象的数量。

访问 Flyweight 对象

访问 flyweight 对象通常涉及工厂数据结构中的查找操作。使用哈希映射的良好实现的工厂,访问 flyweight 对象的时间复杂度为O(1)。如果工厂使用线性搜索或其他效率较低的方法,则时间复杂度可能为 O(n),其中 n 是 flyweight 对象的数量。

空间复杂度

Flyweight 对象的内存使用情况

flyweight 对象本身的内存使用情况取决于其内部状态的大小以及创建的唯一flyweight 对象的数量。每个 flyweight 对象都占用内存来存储其内部状态,例如符号、大小和颜色。假设每个 flyweight 对象具有恒定大小的内部状态,则存储 n 个唯一 flyweight 对象的内存使用情况为O(n)

Flyweight 工厂的内存使用情况

flyweight 工厂的内存使用情况取决于存储的flyweight 对象的数量以及工厂本身的开销。工厂存储对 flyweight 对象的引用,其内存使用情况与创建的 flyweights 数量成正比。此外,工厂可能还会因用于存储的数据结构(如哈希映射或向量)而产生开销。

假设工厂的数据结构的内存使用情况为O(n),其中 n 是 flyweight 对象的数量,则总内存使用情况也为O(n)

同样,用于存储 flyweight 对象和管理工厂的内存使用情况可以与创建的唯一 flyweight 对象的数量成比例。Flyweight 模式的高效实现有助于最大限度地减少时间和空间开销,使其适用于需要有效管理大量细粒对象的应用程序。

方法二:带对象池的 Flyweight

Flyweight 模式与对象池相结合,旨在通过重用 flyweight 对象而不是每次请求时都创建新对象来优化内存使用。该模式在需要高效管理大量细粒对象的情况下特别有用,例如在文本处理图形应用程序中。

它管理着一个 flyweight 对象池。当请求一个新的 flyweight 对象时,工厂会检查池中是否已有一个具有相同内部状态的对象。如果可用,它将返回现有对象;否则,它将创建一个新对象。当 flyweight 对象不再需要时,它将被释放回池中以供重用。

对象池模式会预先创建一个可重用对象池,以避免频繁创建和销毁对象的开销。池中的对象在需要时会被重用,从而减少了对象创建和销毁所需的时间和资源。

在对象池模式的上下文中

Object Pool(对象池): 管理可重用对象集合。它提供从池中获取和释放对象的方法。

Client(客户端): 客户端在需要时从池中请求对象,并在不再需要时将它们释放回池中。

程序

输出

Character: A Size: 12 Color: Black
Character: B Size: 10 Color: Red
Character: A Size: 12 Color: Black

说明

Flyweight 接口和 Concrete Flyweight

Character 接口定义了 flyweight 对象的通用操作。它声明了一个 print() 方法,该方法由具体的flyweight 类实现。在本例中,ConcreteCharacter 是 Character 接口的具体实现。它代表单个 flyweight 对象,并包含诸如字符符号、大小和颜色之类的内部状态。

带对象池的 Flyweight Factory

CharacterFactory 类作为创建和管理 flyweight 对象的工厂。它包括一个对象池,用于高效地存储和重用 flyweight 对象。工作原理如下:

getCharacter(): 此方法负责创建或重用 flyweight 对象。当请求一个新的 flyweight 时,工厂会检查池中是否有请求符号的可用对象。如果没有,它将创建一个新的ConcreteCharacter 对象。如果有,它将从池中检索对象并将其移除出池向量。

releaseCharacter(): 当 flyweight 对象不再需要时,可以使用此方法将其释放回池中。它将释放的对象添加回池向量,以便以后重用。

Object Pool(对象池): 工厂的池成员是一个无序映射,其中键是字符符号,值是flyweight 对象的向量。此池允许按符号高效存储和检索 flyweight 对象。

主函数

在 main() 函数中,我们演示了如何使用带对象池的 Flyweight 模式。我们创建了CharacterFactory 的实例,并使用它获取代表字符“A”和“B”的 flyweight 对象。然后,我们使用 print() 方法打印它们的属性。我们还演示了在请求另一个“A”字符时会重用相同的 flyweight 对象。最后,我们将flyweight 对象释放回工厂的对象池。

此实现有效地管理了大量细粒对象,使其适用于内存优化和性能至关重要的应用程序。

复杂度分析

时间复杂度

创建 Flyweight 对象

使用 getCharacter() 创建 flyweight 对象时:

如果请求的符号的对象池为空,则创建新的 flyweight 对象需要恒定的时间,O(1)。如果池中已经包含请求的符号的 flyweight 对象,则从池中检索现有对象也需要恒定的时间,O(1)

释放 Flyweight 对象

使用releaseCharacter()将 flyweight 对象释放回池也需要恒定的时间,O(1)。

主函数

在 main() 函数中创建和打印 flyweight 对象需要恒定的时间,O(1),因为它们是简单的操作,没有循环或嵌套结构。

提供的代码对于大多数操作(包括创建、访问和释放 flyweight 对象)的时间复杂度都是恒定的,O(1)。这种效率是通过使用对象池有效管理 flyweight 对象来实现的。

空间复杂度

Flyweight 对象

存储 flyweight 对象的内存使用情况取决于创建的唯一 flyweight 对象的数量。每个flyweight 对象都占用内存来存储其内部状态。假设每个 flyweight 对象具有恒定大小的内部状态,则存储 n 个唯一 flyweight 对象的内存使用情况为O(n)

对象池

对象池的内存使用情况也取决于存储的 flyweight 对象的数量。对象池使用无序映射实现,其中键是字符符号,值是 flyweight 对象的向量。对象池数据结构的内存使用情况为O(n),其中 n 是创建的唯一 flyweight 对象的数量。此外,无序映射内的向量容器存在空间开销,但这通常比flyweight 对象本身小。

内存使用情况取决于创建的唯一 flyweight 对象的数量,并且与对象池中存储的对象数量成正比。假设每个 flyweight 对象具有恒定的内部状态,则总内存使用情况为 O(n),其中 n 是创建的唯一 flyweight 对象的数量。

此实现演示了带对象池的 Flyweight 模式在优化内存使用和提高性能方面的有效性,尤其是在需要高效管理大量细粒对象的场景中。

Flyweight 模式的用途

Flyweight 模式是一种结构性设计模式,旨在通过在多个相似对象之间共享对象状态的通用部分来优化内存使用和提高性能。在需要高效管理大量细粒对象的情况下,此模式特别有用。让我们探讨一些 Flyweight 模式的常见用例:

文本编辑器和文字处理器

在文本编辑应用程序中,文档中的每个字符都可以表示为 flyweight 对象。由于许多字符共享字体、大小和颜色等通用属性,Flyweight 模式允许在多个字符之间共享这些属性,从而减少内存使用。

图形和绘图应用程序

图形设计软件通常处理线条、形状和符号等对象。通过使用 flyweight 对象表示这些对象,可以共享位置、颜色和形状等通用属性。这有助于减少内存开销,尤其是在处理大型绘图或复杂场景时。

游戏开发

在游戏开发中,Flyweight 模式通常用于管理纹理、精灵和动画等资源。例如,在一个具有许多相似对象(如子弹或敌人)的 2D 游戏中,该模式可以通过在实例之间共享通用属性来优化内存使用。

用户界面设计

图形用户界面 (GUI) 框架经常使用 flyweight 对象来表示按钮、标签和图标等 UI 元素。通过共享外观和行为等通用属性,该模式有助于创建响应迅速且内存高效的界面。

缓存

Flyweight 对象可用于缓存机制,以高效地存储频繁访问的数据。在 Web 应用程序中,缓存频繁访问的数据对象有助于减少数据库查询并提高性能。

文档结构

文档处理应用程序可以使用 Flyweight 模式来高效地管理大型文档的结构。例如,在 XML 或 JSON 解析器中,flyweight 对象可以表示文档中的元素或节点,共享诸如标签名称和属性等通用属性。

符号表和字典

Flyweight 对象可用于高效地实现符号表和字典。例如,在编译器或解释器中,flyweight 对象可以表示代码中的标识符、关键字或符号,从而减少内存消耗。