设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
享元模式
> 享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。 ![享元设计模式](/media/202203/2022-03-05_2204140.13411605764377266.png) 享元模式(Flyweight Pattern、缓存、Cache、Flyweight)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。 享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。 Direct3D 和 OpenGL 都可以做[*实例渲染*](http://en.wikipedia.org/wiki/Geometry_instancing),这是由显卡直接实现的,意味着享元模式也许是**唯一的有硬件支持的 GoF 设计模式**。 ## 问题 假如你希望在长时间工作后放松一下, 所以开发了一款简单的游戏: 玩家们在地图上移动并相互射击。 你决定实现一个真实的粒子系统, 并将其作为游戏的特色。 大量的子弹、 导弹和爆炸弹片会在整个地图上穿行, 为玩家提供紧张刺激的游戏体验。 开发完成后, 你推送提交了最新版本的程序, 并在编译游戏后将其发送给了一个朋友进行测试。 尽管该游戏在你的电脑上完美运行, 但是你的朋友却无法长时间进行游戏: 游戏总是会在他的电脑上运行几分钟后崩溃。 在研究了几个小时的调试消息记录后, 你发现导致游戏崩溃的原因是内存容量不足。 朋友的设备性能远比不上你的电脑, 因此游戏运行在他的电脑上时很快就会出现问题。 真正的问题与粒子系统有关。 每个粒子 (一颗子弹、 一枚导弹或一块弹片) 都由包含完整数据的独立对象来表示。 当玩家在游戏中鏖战进入高潮后的某一时刻, 游戏将无法在剩余内存中载入新建粒子, 于是程序就崩溃了。 ![享元模式问题](/media/202203/2022-03-05_2209130.7464881848546324.png) ## 解决方案 仔细观察 粒子 Particle 类, 你可能会注意到颜色 (color) 和精灵图 (sprite)这两个成员变量所消耗的内存要比其他变量多得多。 更糟糕的是, 对于所有的粒子来说, 这两个成员变量所存储的数据几乎完全一样 (比如所有子弹的颜色和精灵图都一样)。 ![享元模式的解决方案](/media/202203/2022-03-05_2210360.4194138513442709.png) 每个粒子的另一些状态 (坐标、 移动矢量和速度) 则是不同的。 因为这些成员变量的数值会不断变化。 这些数据代表粒子在存续期间不断变化的情景, 但每个粒子的颜色和精灵图则会保持不变。 对象的常量数据通常被称为内在状态, 其位于对象中, 其他对象只能读取但不能修改其数值。 而对象的其他状态常常能被其他对象 “从外部” 改变, 因此被称为外在状态。 享元模式建议不在对象中存储外在状态, 而是将其传递给依赖于它的一个特殊方法。 程序只在对象中保存内在状态, 以方便在不同情景下重用。 这些对象的区别仅在于其内在状态 (与外在状态相比, 内在状态的变体要少很多), 因此你所需的对象数量会大大削减。 ![享元模式的解决方案](/media/202203/2022-03-05_2213070.27345686583833273.png) 让我们回到游戏中。 假如能从粒子类中抽出外在状态, 那么我们只需三个不同的对象 (子弹、 导弹和弹片) 就能表示游戏中的所有粒子。 你现在很可能已经猜到了, 我们将这样一个**仅存储内在状态的对象称为享元**。 ### 外在状态存储 那么外在状态会被移动到什么地方呢? 总得有类来存储它们, 对不对? 在大部分情况中, 它们会被移动到容器对象中, 也就是我们应用享元模式前的聚合对象中。 在我们的例子中, 容器对象就是主要的 游戏 Game 对象, 其会将所有粒子存储在名为 粒子 particles 的成员变量中。 为了能将外在状态移动到这个类中, 你需要创建多个数组成员变量来存储每个粒子的坐标、 方向矢量和速度。 除此之外, 你还需要另一个数组来存储指向代表粒子的特定享元的引用。 这些数组必须保持同步, 这样你才能够使用同一索引来获取关于某个粒子的所有数据。 ![享元模式的解决方案](/media/202203/2022-03-05_2218050.6210512803886538.png) 更优雅的解决方案是创建独立的情景类来存储外在状态和对享元对象的引用。 在该方法中, 容器类只需包含一个数组。 稍等! 这样的话情景对象数量不是会和不采用该模式时的对象数量一样多吗? 的确如此, 但这些对象要比之前小很多。 消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。 现在, 一个享元大对象会被上千个情境小对象复用, 因此无需再重复存储数千个大对象的数据。 ### 享元与不可变性 由于享元对象可在不同的情景中使用, 你必须**确保其状态不能被修改**。 **享元类的状态只能由构造函数的参数进行一次性初始化, 它不能对其他对象公开其设置器或公有成员变量**。 ### 享元工厂 为了能更方便地访问各种享元, 你可以创建一个工厂方法来管理已有享元对象的缓存池。 工厂方法从客户端处接收目标享元对象的内在状态作为参数, 如果它能在缓存池中找到所需享元, 则将其返回给客户端; 如果没有找到, 它就会新建一个享元, 并将其添加到缓存池中。 你可以选择在程序的不同地方放入该函数。 最简单的选择就是将其放置在享元容器中。 除此之外, 你还可以新建一个工厂类, 或者创建一个静态的工厂方法并将其放入实际的享元类中。 ## 目的 为了节约内存的使用,享元模式会尽量使类似的对象共享内存。在大量类似对象被使用的情况中这是十分必要的。常用做法是在外部数据结构中保存类似对象的状态,并在需要时将他们传递给享元对象。 ## 介绍 **意图:** 运用共享技术有效地支持大量细粒度的对象。 **主要解决:** 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。 **何时使用:** 1. 系统中有大量对象。 2. 这些对象消耗大量内存。 3. 这些对象的状态大部分可以外部化。 4. 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5. 系统不依赖于这些对象身份,这些对象是不可分辨的。 **如何解决:** 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。 **关键代码:** **用 HashMap 存储这些对象**。 **应用实例:** 1. JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2. 数据库的数据池。 **优点:** 大大减少对象的创建,降低系统的内存,使效率提高。 **缺点:** 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。 **使用场景:** 1. 系统有大量相似对象。 2. 需要缓冲池的场景。 **注意事项:** 1. 注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2. 这些类必须有一个工厂对象加以控制。 ## 结构 ![享元设计模式的结构](/media/202203/2022-03-05_2226410.01813825376446787.png) - 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。 - 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。**同一享元对象可在许多不同情景中使用**。 **享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”**。 - 情景 (Context) 类包含原始对象中各不相同的外在状态。 **情景与享元对象组合在一起就能表示原始对象的全部状态**。 - 通常情况下, 原始对象的行为会保留在享元类中。 **因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。** - 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。 ### 实例 我们将创建一个 *Shape* 接口和实现了 *Shape* 接口的实体类 *Circle* 。下一步是定义工厂类 *ShapeFactory* 。 *ShapeFactory* 有一个 *Circle* 的 *HashMap* ,其中键名为 *Circle* 对象的颜色。无论何时接收到请求,都会创建一个特定颜色的圆。*ShapeFactory* 检查它的 *HashMap* 中的 circle 对象,如果找到 *Circle* 对象,则返回该对象,否则将创建一个存储在 hashmap 中以备后续使用的新对象,并把该对象返回到客户端。 *FlyWeightPatternDemo* 类使用 *ShapeFactory* 来获取 *Shape* 对象。它将向 *ShapeFactory* 传递信息( *red / green / blue/ black / white* ),以便获取它所需对象的颜色。 ![享元模式的 UML 图](https://www.runoob.com/wp-content/uploads/2014/08/20201015-fiyweight.svg) ## 伪代码 在本例中, 享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。 ![享元模式的示例](/media/202203/2022-03-05_2240270.33373519132208207.png) 该模式从主要的 树 Tree 类中抽取内在状态, 并将其移动到享元类 树种类 TreeType 之中。 最初程序需要在多个对象中存储相同数据, 而现在仅需在几个享元对象中保存数据, 然后在作为情景的 树对象中连入享元即可。 客户端代码使用享元工厂创建树对象并封装搜索指定对象的复杂行为, 并能在需要时复用对象。 ``` // 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一 // 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜 // 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗 // 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象 // 中,然后让众多的单个树对象去引用它。 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. 对象中包含可抽取且能在多个对象间共享的重复状态。 ## 实现方式 1. 将需要改写为享元的类成员变量拆分为两个部分: - 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。 - 外在状态: 包含每个对象各自不同的情景数据的成员变量 2. 保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。 3. 找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。 4. 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。 5. 客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。 ## 优点 - 如果程序中有很多相似对象, 那么你将可以节省大量内存。 ## 缺点 - 你可能需要牺牲执行速度来换取内存, 因为他人**每次调用享元方法时都需要重新计算部分情景数据**。 - 代码会变得更加复杂。 团队中的新成员总是会问: “为什么要像这样拆分一个实体的状态?”。 ## 与其他模式的关系 - 你可以使用享元模式实现组合模式树的共享叶节点以节省内存。 - 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。 - 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。 - 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。 - 单例对象可以是可变的。 享元对象是不可变的。 ## 代码示例 ``` /** * The Flyweight stores a common portion of the state (also called intrinsic * state) that belongs to multiple real business entities. The Flyweight accepts * the rest of the state (extrinsic state, unique for each entity) via its * method parameters. */ class Flyweight { private sharedState: any; constructor(sharedState: any) { this.sharedState = sharedState; } public operation(uniqueState): void { const s = JSON.stringify(this.sharedState); const u = JSON.stringify(uniqueState); console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`); } } /** * The Flyweight Factory creates and manages the Flyweight objects. It ensures * that flyweights are shared correctly. When the client requests a flyweight, * the factory either returns an existing instance or creates a new one, if it * doesn't exist yet. */ class FlyweightFactory { private flyweights: {[key: string]: Flyweight} = <any>{}; constructor(initialFlyweights: string[][]) { for (const state of initialFlyweights) { this.flyweights[this.getKey(state)] = new Flyweight(state); } } /** * Returns a Flyweight's string hash for a given state. */ private getKey(state: string[]): string { return state.join('_'); } /** * Returns an existing Flyweight with a given state or creates a new one. */ public getFlyweight(sharedState: string[]): Flyweight { const key = this.getKey(sharedState); if (!(key in this.flyweights)) { console.log('FlyweightFactory: Can\'t find a flyweight, creating new one.'); this.flyweights[key] = new Flyweight(sharedState); } else { console.log('FlyweightFactory: Reusing existing flyweight.'); } return this.flyweights[key]; } public listFlyweights(): void { const count = Object.keys(this.flyweights).length; console.log(`\nFlyweightFactory: I have ${count} flyweights:`); for (const key in this.flyweights) { console.log(key); } } } /** * The client code usually creates a bunch of pre-populated flyweights in the * initialization stage of the application. */ const factory = new FlyweightFactory([ ['Chevrolet', 'Camaro2018', 'pink'], ['Mercedes Benz', 'C300', 'black'], ['Mercedes Benz', 'C500', 'red'], ['BMW', 'M5', 'red'], ['BMW', 'X6', 'white'], // ... ]); factory.listFlyweights(); // ... function addCarToPoliceDatabase( ff: FlyweightFactory, plates: string, owner: string, brand: string, model: string, color: string, ) { console.log('\nClient: Adding a car to database.'); const flyweight = ff.getFlyweight([brand, model, color]); // The client code either stores or calculates extrinsic state and passes it // to the flyweight's methods. flyweight.operation([plates, owner]); } addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red'); addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red'); factory.listFlyweights(); ``` 输出: ``` FlyweightFactory: I have 5 flyweights: Chevrolet_Camaro2018_pink Mercedes Benz_C300_black Mercedes Benz_C500_red BMW_M5_red BMW_X6_white Client: Adding a car to database. FlyweightFactory: Reusing existing flyweight. Flyweight: Displaying shared (["BMW","M5","red"]) and unique (["CL234IR","James Doe"]) state. Client: Adding a car to database. FlyweightFactory: Can't find a flyweight, creating new one. Flyweight: Displaying shared (["BMW","X1","red"]) and unique (["CL234IR","James Doe"]) state. FlyweightFactory: I have 6 flyweights: Chevrolet_Camaro2018_pink Mercedes Benz_C300_black Mercedes Benz_C500_red BMW_M5_red BMW_X6_white BMW_X1_red ```
追风者
2022年3月31日 20:45
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码