前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2023 跟我一起学设计模式: 享元模式 (Cache)

2023 跟我一起学设计模式: 享元模式 (Cache)

作者头像
用户1418987
发布2023-10-16 09:43:35
1360
发布2023-10-16 09:43:35
举报
文章被收录于专栏:codercoder

享元模式

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

问题

假如你希望在长时间工作后放松一下, 所以开发了一款简单的游戏: 玩家们在地图上移动并相互射击。 你决定实现一个真实的粒子系统, 并将其作为游戏的特色。 大量的子弹、 导弹和爆炸弹片会在整个地图上穿行, 为玩家提供紧张刺激的游戏体验。

开发完成后, 你推送提交了最新版本的程序, 并在编译游戏后将其发送给了一个朋友进行测试。 尽管该游戏在你的电脑上完美运行, 但是你的朋友却无法长时间进行游戏: 游戏总是会在他的电脑上运行几分钟后退出。 在研究了几个小时的调试消息记录后, 你发现导致游戏退出的原因是内存容量不足。 朋友的设备性能远比不上你的电脑, 因此游戏运行在他的电脑上时很快就会出现问题。

真正的问题与粒子系统有关。 每个粒子 (一颗子弹、 一枚导弹或一块弹片) 都由包含完整数据的独立对象来表示。 当玩家在游戏中鏖战进入高潮后的某一时刻, 游戏将无法在剩余内存中载入新建粒子, 于是程序就退出了。

解决方案

仔细观察 粒子Particle类, 你可能会注意到颜色 (color) 和精灵图 (sprite)这两个成员变量所消耗的内存要比其他变量多得多。 更糟糕的是, 对于所有的粒子来说, 这两个成员变量所存储的数据几乎完全一样 (比如所有子弹的颜色和精灵图都一样)。

每个粒子的另一些状态 (坐标、 移动矢量和速度) 则是不同的。 因为这些成员变量的数值会不断变化。 这些数据代表粒子在存续期间不断变化的情景, 但每个粒子的颜色和精灵图则会保持不变。

对象的常量数据通常被称为内在状态, 其位于对象中, 其他对象只能读取但不能修改其数值。 而对象的其他状态常常能被其他对象 “从外部” 改变, 因此被称为外在状态

享元模式建议不在对象中存储外在状态, 而是将其传递给依赖于它的一个特殊方法。 程序只在对象中保存内在状态, 以方便在不同情景下重用。 这些对象的区别仅在于其内在状态 (与外在状态相比, 内在状态的变体要少很多), 因此你所需的对象数量会大大削减。

让我们回到游戏中。 假如能从粒子类中抽出外在状态, 那么我们只需三个不同的对象 (子弹、 导弹和弹片) 就能表示游戏中的所有粒子。 你现在很可能已经猜到了, 我们将这样一个仅存储内在状态的对象称为享元。

外在状态存储

那么外在状态会被移动到什么地方呢? 总得有类来存储它们, 对不对? 在大部分情况中, 它们会被移动到容器对象中, 也就是我们应用享元模式前的聚合对象中。

在我们的例子中, 容器对象就是主要的 游戏Game对象, 其会将所有粒子存储在名为 粒子particles的成员变量中。 为了能将外在状态移动到这个类中, 你需要创建多个数组成员变量来存储每个粒子的坐标、 方向矢量和速度。 除此之外, 你还需要另一个数组来存储指向代表粒子的特定享元的引用。 这些数组必须保持同步, 这样你才能够使用同一索引来获取关于某个粒子的所有数据。

更优雅的解决方案是创建独立的情景类来存储外在状态和对享元对象的引用。 在该方法中, 容器类只需包含一个数组。

稍等! 这样的话情景对象数量不是会和不采用该模式时的对象数量一样多吗? 的确如此, 但这些对象要比之前小很多。 消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。 现在, 一个享元大对象会被上千个情境小对象复用, 因此无需再重复存储数千个大对象的数据。

享元与不可变性

由于享元对象可在不同的情景中使用, 你必须确保其状态不能被修改。 享元类的状态只能由构造函数的参数进行一次性初始化, 它不能对其他对象公开其设置器或公有成员变量。

享元工厂

为了能更方便地访问各种享元, 你可以创建一个工厂方法来管理已有享元对象的缓存池。 工厂方法从客户端处接收目标享元对象的内在状态作为参数, 如果它能在缓存池中找到所需享元, 则将其返回给客户端; 如果没有找到, 它就会新建一个享元, 并将其添加到缓存池中。

你可以选择在程序的不同地方放入该函数。 最简单的选择就是将其放置在享元容器中。 除此之外, 你还可以新建一个工厂类, 或者创建一个静态的工厂方法并将其放入实际的享元类中。

享元模式结构

  1. 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
  2. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
  3. 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
  5. 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

伪代码

在本例中, 享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。

该模式从主要的 Tree类中抽取内在状态, 并将其移动到享元类 树种类Tree­Type之中。

最初程序需要在多个对象中存储相同数据, 而现在仅需在几个享元对象中保存数据, 然后在作为情景的 对象中连入享元即可。 客户端代码使用享元工厂创建树对象并封装搜索指定对象的复杂行为, 并能在需要时复用对象。

代码语言:javascript
复制
// 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一
// 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜
// 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗
// 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象
// 中,然后让众多的单个树对象去引用它。
class TreeType is
    field name
    field color
    field texture
    constructor TreeType(name, color, texture) { …… }
    method draw(canvas, x, y) is
        // 1. 创建特定类型、颜色和纹理的位图。
        // 2. 在画布坐标 (X,Y) 处绘制位图。

// 享元工厂决定是否复用已有享元或者创建一个新的对象。
class TreeFactory is
    static field treeTypes: collection of tree types
    static method getTreeType(name, color, texture) is
        type = treeTypes.find(name, color, texture)
        if (type == null)
            type = new TreeType(name, color, texture)
            treeTypes.add(type)
        return type

// 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们
// 体积很小:仅有两个整型坐标和一个引用成员变量。
class Tree is
    field x,y
    field type: TreeType
    constructor Tree(x, y, type) { …… }
    method draw(canvas) is
        type.draw(canvas, this.x, this.y)

// 树(Tree)和森林(Forest)类是享元的客户端。如果不打算继续对树类进行开
// 发,你可以将它们合并。
class Forest is
    field trees: collection of Trees

    method plantTree(x, y, name, color, texture) is
        type = TreeFactory.getTreeType(name, color, texture)
        tree = new Tree(x, y, type)
        trees.add(tree)

    method draw(canvas) is
        foreach (tree in trees) do
            tree.draw(canvas)

享元模式适合应用场景

仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

应用该模式所获的收益大小取决于使用它的方式和情景。 它在下列情况中最有效:

  • 程序需要生成数量巨大的相似对象
  • 这将耗尽目标设备的所有内存
  • 对象中包含可抽取且能在多个对象间共享的重复状态。

实现方式

  1. 将需要改写为享元的类成员变量拆分为两个部分:
  2. 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。
  3. 外在状态: 包含每个对象各自不同的情景数据的成员变量
  4. 保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。
  5. 找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。
  6. 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。
  7. 客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。

享元模式优缺点

  • 如果程序中有很多相似对象, 那么你将可以节省大量内存。
  • 你可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。
  • 代码会变得更加复杂。 团队中的新成员总是会问: “为什么要像这样拆分一个实体的状态?”。

代码示例

Go 享元模式讲解和代码示例

享元是一种结构型设计模式, 它允许你在消耗少量内存的情况下支持大量对象。

模式通过共享多个对象的部分状态来实现上述功能。 换句话来说, 享元会将不同对象的相同数据进行缓存以节省内存。

概念示例

在游戏 《反恐精英》 中, 恐怖分子和反恐精英身着不同类型的衣物。 为了简便起见, 我们就假设双方都各有一种服装类型。 服装对象嵌入在玩家对象之中, 如下所示。

下面是玩家的结构体。 我们可以看到, 服装对象是嵌入在玩家结构体之中的:

代码语言:javascript
复制
type player struct {
  dress      dress
  playerType string // 可为 T 或 CT
  lat        int
  long       int
}

假设目前有 5 名恐怖分子和 5 名反恐精英, 一共是 10 名玩家。 那么关于服装, 我们就有两个选项了。

  1. 10 个玩家对象各自创建不同的服装对象, 并将其嵌入。 总共会创建 10 个服装对象。
  2. 我们创建两个服装对象:
  3. 单一恐怖分子服装对象: 其将在 5 名恐怖分子之间共享。
  4. 单一反恐精英服装对象: 其将在 5 名反恐精英之间共享。

你可以看到, 方法 1 中我们总共创建了 10 个服装对象; 方法 2 中则只有 2 个服装对象。 第二种方法, 就是我们所遵循的享元设计模式。 我们所创建的 2 个服装对象被称为是享元对象。

享元模式会从对象中提取出公共部分并创建享元对象。 这些享元对象 (服装) 随后可在多个对象 (玩家) 中分享。 这极大地减少了服装对象的数量, 更棒的是即便你创建了更多玩家, 也只需这么两个服装对象就足够了。

在享元模式中, 我们会将享元对象存储在 map 容器中。 每当创建共享享元对象的其他对象时, 都会从 map 容器中获取享元对象。

下面让我们来看看此类安排的内部状态和外部状态:

  • 内部状态: 内部状态的服装可在多个恐怖分子和反恐精英对象间共享。
  • 外部状态: 玩家位置和玩家所使用的武器就是外部状态, 因为其在每个对象中都是不同的。
dressFactory.go: 享元工厂
代码语言:javascript
复制
package main

import "fmt"

const (
  //TerroristDressType terrorist dress type
  TerroristDressType = "tDress"
  //CounterTerrroristDressType terrorist dress type
  CounterTerrroristDressType = "ctDress"
)

var (
  dressFactorySingleInstance = &DressFactory{
    dressMap: make(map[string]Dress),
  }
)

type DressFactory struct {
  dressMap map[string]Dress
}

func (d *DressFactory) getDressFactoryByType(dressType string) (Dress, error) {
  if d.dressMap[dressType] != nil {
    return d.dressMap[dressType], nil
  }
  if dressType == TerroristDressType {
    d.dressMap[dressType] = newTerrorisDress()
    return d.dressMap[dressType], nil
  }
  if dressType == CounterTerrroristDressType {
    d.dressMap[dressType] = newCounterTerrroristDress()
    return d.dressMap[dressType], nil
  }
  return nil, fmt.Errorf("Wrong dress type passed")
}

func getDressFactorySingleInstance() *DressFactory {
  return dressFactorySingleInstance
}
dress.go: 享元接口
代码语言:javascript
复制
package main

// 享元接口
type Dress interface {
  getColor() string
}
terroristDress.go: 具体享元对象
代码语言:javascript
复制
package main

type TerroristDress struct {
  color string
}

func (t *TerroristDress) getColor() string {
  return t.color
}

func newTerrorisDress() *TerroristDress {
  return &TerroristDress{color: "red"}
}
counterTerroristDress.go: 具体享元对象
代码语言:javascript
复制
package main

// 享元接口
type CounterTerrroristDress struct {
  color string
}

func (c *CounterTerrroristDress) getColor() string {
  return c.color
}

func newCounterTerrroristDress() *CounterTerrroristDress {
  return &CounterTerrroristDress{color: "green"}
}
player.go: 背景
代码语言:javascript
复制
package main

type Player struct {
  dress      Dress
  playerType string
  lat        int
  long       int
}

func newPlayer(playerType, dressType string) *Player {
  dress, _ := getDressFactorySingleInstance().getDressFactoryByType(dressType)
  return &Player{
    dress:      dress,
    playerType: playerType,
  }
}

func (p *Player) newLocation(lat, long int) {
  p.lat = lat
  p.long = long
}
game.go: 客户端代码
代码语言:javascript
复制
package main

type game struct {
  terrorists        []*Player
  counterTerrorists []*Player
}

func newGame() *game {
  return &game{
    terrorists:        make([]*Player, 1),
    counterTerrorists: make([]*Player, 01),
  }
}

func (c *game) addTerrorist(dressType string) {
  player := newPlayer("T", dressType)
  c.terrorists = append(c.terrorists, player)
  return
}

func (c *game) addCounterTerrorist(dressType string) {
  player := newPlayer("CT", dressType)
  c.counterTerrorists = append(c.counterTerrorists, player)
  return
}
main.go: 客户端代码
代码语言:javascript
复制
package main

import "fmt"

func main() {
  game := newGame()

  //Add Terrorist
  game.addTerrorist(TerroristDressType)
  game.addTerrorist(TerroristDressType)
  game.addTerrorist(TerroristDressType)
  game.addTerrorist(TerroristDressType)

  //Add CounterTerrorist
  game.addCounterTerrorist(CounterTerrroristDressType)
  game.addCounterTerrorist(CounterTerrroristDressType)
  game.addCounterTerrorist(CounterTerrroristDressType)

  dressFactoryInstance := getDressFactorySingleInstance()

  for dressType, dress := range dressFactoryInstance.dressMap {
    fmt.Printf("DressColorType: %s\nDressColor: %s\n", dressType, dress.getColor())
  }
}
output.txt: 执行结果
代码语言:javascript
复制
DressColorType: tDress
DressColor: red
DressColorType: ctDress
DressColor: green

相关设计模式

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 享元模式
    • 问题
      • 解决方案
        • 享元模式结构
          • 伪代码
            • 享元模式适合应用场景
              • 实现方式
                • 享元模式优缺点
                  • 代码示例
                    • dressFactory.go: 享元工厂
                    • dress.go: 享元接口
                    • terroristDress.go: 具体享元对象
                    • counterTerroristDress.go: 具体享元对象
                    • player.go: 背景
                    • game.go: 客户端代码
                    • main.go: 客户端代码
                    • output.txt: 执行结果
                • Go 享元模式讲解和代码示例
                • 概念示例
                  • 相关设计模式
                  相关产品与服务
                  对象存储
                  对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档