享元模式(Flyweight)

news2024/9/21 1:53:30

别名

缓存(Cache)。

定义

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

前言

1. 问题

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

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

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

2. 解决方案

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

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

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

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

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

3. 外在状态存储

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

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

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

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

4. 享元与不可变性

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

5. 享元工厂

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

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

结构

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

适用场景

仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式,应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:

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

实现方式

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

优点

如果程序中有很多相似对象,那么你将可以节省大量内存。

缺点

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

Flyweight.hpp

#ifndef BB5768A1_8406_419C_A0CC_AF7A6D2FF7BD
#define BB5768A1_8406_419C_A0CC_AF7A6D2FF7BD
#include "string"

using namespace std;

class TreeType{
    private:
        string name_;
        string color_;
        string texture_;
    public:
        TreeType(string n,string c,string t):name_(n),color_(c),texture_(t){}
        void draw(string canvas,double x,double y){
            return ;
        }
};
#endif /* BB5768A1_8406_419C_A0CC_AF7A6D2FF7BD */

Context.hpp

#ifndef BBA423F7_54A3_464A_BF06_767F118615A3
#define BBA423F7_54A3_464A_BF06_767F118615A3

#include <string>
#include "Flyweight.hpp"

// 情景对象包含树类型的「外在状态」, 程序中可以创建数十亿个此类对象, 因为它们体积很小: 仅有两个浮点坐标类型和一个引用成员变量
class Tree {
 public:
    Tree(double x, double y, TreeType* t) : x_(x), y_(y), type_(t) {}
    void draw(std::string canvas) {
        return type_->draw(canvas, x_, y_);
    }

 private:
    double x_;
    double y_;
    TreeType* type_;
};


#endif /* BBA423F7_54A3_464A_BF06_767F118615A3 */

FlyweightFactory.hpp

#ifndef B84B346F_9417_4F82_8890_248455DDFD3E
#define B84B346F_9417_4F82_8890_248455DDFD3E
#include <map>
#include <string>
#include <mutex>
#include "Flyweight.hpp"

// 享元工厂: 决定是否复用已有享元或者创建一个新的对象, 同时它也是一个单例模式
class TreeFactory {
 public:
    static TreeFactory* getInstance() {
        if (instance_ == nullptr) {
            mutex_.lock();
            if (instance_ == nullptr) {
                instance_ = new TreeFactory();
            }
            mutex_.unlock();
        }
        return instance_;
    }
    TreeType* getTreeType(std::string name, std::string color, std::string texture) {
        std::string key = name + "_" + color + "_" + texture;
        auto iter = tree_types_.find(key);
        if (iter == tree_types_.end()) {
            // 新的tree type
            TreeType* new_tree_type = new TreeType(name, color, texture);
            tree_types_[key] = new_tree_type;
            return new_tree_type;
        } else {
            // 已存在的tree type
            return iter->second;
        }
    }

 private:
    TreeFactory() {}
    static TreeFactory* instance_;
    static std::mutex mutex_;

    // 共享池, 其中key格式为name_color_texture
    std::map<std::string, TreeType*> tree_types_;
};


#endif /* B84B346F_9417_4F82_8890_248455DDFD3E */

FlyweightFactory.cpp

#include "FlyweightFactory.hpp"

TreeFactory* TreeFactory::instance_ = nullptr;
std::mutex TreeFactory::mutex_;

Client.hpp

#ifndef FB57BBB3_425B_46EA_BE03_32C3618A661F
#define FB57BBB3_425B_46EA_BE03_32C3618A661F
#include <vector>
#include <iostream>
#include <string>
#include "FlyweightFactory.hpp"
#include "Context.hpp"

// Forest包含数量及其庞大的Tree
class Forest {
 public:
    void planTree(double x, double y, std::string name, std::string color, std::string texture) {
        TreeType* type = TreeFactory::getInstance()->getTreeType(name, color, texture);
        Tree tree = Tree(x, y, type);
        trees_.push_back(tree);
    }
    void draw() {
        for (auto tree : trees_) {
            tree.draw("canvas");
        }
    }

 private:
    std::vector<Tree> trees_;
};


#endif /* FB57BBB3_425B_46EA_BE03_32C3618A661F */

main.cpp

#include "Client.hpp"

int main() {
    Forest* forest = new Forest();

    // 在forest中种植很多棵树
    for (int i = 0; i < 500; i++) {
        for (int j = 0; j < 500; j++) {
            double x = i;
            double y = j;
            // 树类型1: 红色的杉树
            forest->planTree(x, y, "杉树", "红色", "");
            // 树类型2: 绿色的榕树
            forest->planTree(x, y, "榕树", "绿色", "");
            // 树类型3: 白色的桦树
            forest->planTree(x, y, "桦树", "白色", "");
        }
    }

    forest->draw();

    delete forest;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/690983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

互联网+洗鞋店软件多门店多网点预约下单小程序

互联网洗鞋店软件小程序功能介绍: 1.用户端&#xff08;上门取件、送货到店、寄存网点&#xff09; 2.取货员端&#xff08;取件员拍照&#xff0c;清洗过程&#xff0c;包装拍照&#xff09; 3.多门店管理&#xff08;用户进入小程序&#xff0c;根据定位自动匹配就近门店&…

Golang每日一练(leetDay0106) 超级丑数、右侧小于当前元素的个数

目录 313. 超级丑数 Super Ugly Number &#x1f31f;&#x1f31f; 315. 计算右侧小于当前元素的个数 Count-of-smaller-numbers-after-self &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练…

easypoi 导出word并插入echart图片和文件

一 pom 文件引入&#xff1a;<!-- 目前的版本对应 poi 4.1.2 和 xmlbeans 3.1.0 , poi 3.17 和 xmlbeans 2.6.0 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version&…

新人必看的Java基础知识点大梳理

一个Java程序可以认为是一系列对象的集合&#xff0c;而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。 对象&#xff1a;对象是类的一个实例&#xff0c;有状态和行为。例如&#xff0c;一条狗是一个对象&#xff0c;它的状态有&…

树莓派Pico|SHELL中microPython命令行|pico sdk开发环境搭建|点灯代码|必备开发工具|gcc涉及的include文件目录

文章目录 SHELL中microPython命令行SHELL中基于microPython的控制代码Hello Pico 代码SHELL中简单点灯代码SHELL中循环亮灯代码 基于pico sdk开发环境搭建及点灯代码必备开发工具Mingw-w64&#xff1a;著名C/C编译器GCCarm-none-eabi&#xff1a;交叉编译工具Git&#xff1a;开…

js翻转数组

arr [red, green, "blue", "pink", "purple"];var arr1 [];for (var i 1; i < arr.length; i) {console.log(arr1.length)arr1[arr1.length] arr[arr.length - i];console.log(arr1.length)}console.log(arr1);

echarts饼装图自定义图例和扇形区的文字

最近因为工作需要&#xff0c;需要开发一个大屏&#xff0c;后台给的数据是这个的&#xff0c;echarts是默认将数据data例的name属性作为图例和扇形图上展示文本&#xff0c;这里我需要自定义图例信息和内容&#xff0c;通过这篇文章&#xff0c;记录下如何修改这些内容&#x…

spring IOC详解

一、IOC IoC就是Inversion of Control&#xff0c;控制反转。在Java开发中&#xff0c;IoC意味着将你设计好的类交给系统去控制&#xff0c;而不是在你的类内部控制。这称为控制反转。 下面我们以几个例子来说明什么是IoC。假设我们要设计一个Girl和一个Boy类&#xff0c;其中G…

【PostgreSQL-16新特性之普通用户的保留连接个数(reserved_connections)】

PostgreSQL数据库为了保证在高并发&#xff0c;高连接数情况下某些用户能够正常连接到数据库里&#xff0c;设立了几个用户连接的保留个数。 本文介绍了PostgreSQL16版本前为超级用户保留的连接数&#xff08;superuser_reserved_connections&#xff09;以及PostgreSQL16版本…

【江西省研究生数学建模竞赛】题目之三 植物的多样性 建模方案及参考文献

【江西省研究生数学建模竞赛】题目之三 植物的多样性 建模方案及参考文献 1 题目 2023年江西省研究生数模竞赛题目之三 植物的多样性 植物作为食物链中的生产者&#xff0c;通过光合作用吸收二氧化碳&#xff0c;制造氧气&#xff0c;同时为其他生物提供食物和栖息地&#x…

前端JavaScript入门-day04

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 函数 为什么需要函数 函数使用 函数的声明语法 函数名命名规范 函数的调用语法 函数体 函数传参 声…

Scala集合与Java集合的互转函数asScala与asJava

只有引入隐式转换类 JavaConverters&#xff0c;才能使用Scala集合与Java集合之间的转换函数。 为方便理解&#xff0c;看下面的例子&#xff1a; import scala.collection.JavaConverters._ val javaList java.util.Arrays.asList("hi", "bye") val sca…

Restic文件备份工具

一、Restic介绍 Restic 是一款 GO 语言开发的开源免费且快速、高效和安全的跨平台备份工具。Restic 使用加密技术来保证你的数据安全性和完整性&#xff0c;可以将本地数据加密后传输到指定的存储。Restic 同样支持增量备份&#xff0c;可随时备份和恢复备份。Restic 支持大多数…

一个自动下载网页图片的python小程序

文章目录 1.一些杂七杂八的引入2.实现2.1 安装所需python包2.1.1 requests包2.1.1 BeautifulSoup包 3.源码分享4.效果展示 1.一些杂七杂八的引入 最近是端午节&#xff0c;本人碰巧又刚考完试&#xff08;数学砸了&#xff0c;估分115&#xff0c;别的还行&#xff09; 于是……

Oracle 查询优化改写(第六章)

第六章 使用数字 1 常用聚集函数&#xff08;空值处理&#xff09; 2 生成累计和 --公司为了查看用人成本&#xff0c;需要对员工的工资进行累加&#xff0c; --以便查看员工人数与工资支出之间的对应关系。 SELECT Empno,Ename,Sal,SUM(Sal) Over(ORDER BY Empno) AS 成本累…

绿色能源外交:国际间合作促进可再生能源全球普及

随着全球气候变化的威胁日益凸显&#xff0c;减少碳排放和转向可持续能源已经成为国际社会的共同目标。在这个背景下&#xff0c;绿色能源外交应运而生。绿色能源外交是指国际间合作&#xff0c;通过技术转让、政策协调和资金支持等手段&#xff0c;推动可再生能源在全球范围内…

文字PDF转换为图片格式的PDF

在我们的日常工作和生活中&#xff0c;有时候我们需要对PDF文件进行一些特殊处理。有时候&#xff0c;我们希望将PDF的每一页提取出来作为图片&#xff0c;方便在其他场景中使用&#xff1b;而有时候&#xff0c;我们则需要将PDF内的内容转换为图片格式&#xff0c;以防止他人对…

docker 操作手册

名词解释 images&#xff1a;封装了应用程序的镜像 tag&#xff1a;镜像的标记&#xff0c;一个镜像可以创建多个标记 container&#xff1a;装载镜像并运行 常用命令 查看容器 docker ps -a //查看全部镜像 启动容器 docker start mysql //启动mysql容器 停止容器 doc…

Python篇——数据结构与算法(第七部分:树)

目录 1.树与二叉树 2.树的实例&#xff1a;模拟文件系统 3.二叉树 4.二叉树的遍历 5.二叉搜索树 5.1插入 5.2查询 5.3删除 1.树与二叉树 2.树的实例&#xff1a;模拟文件系统 # 树的实例 class Node:def __init__(self, name, typedir):self.name nameself.type typ…

springboot集成mybatisPlus

1、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-jav…