设计模式学习笔记 - 设计模式与范式 -结构型:7.享元模式(上):享元模式原理和应用

概述

在《设计模式与范式 -结构型:6.组合模式》,讲了组合模式。组合模式并不常用,主要用在数据能表示成树形结构、能通过遍历算法来解决问题的场景中。本章再学习一个不那么常用的模式,享元模式(Flyweight Design Pattern)。这也是学习的最后一个结构型模式。

跟其他所有的设计模式类似,享元模式的原理和实现也非常简单。今天,通过棋牌游戏和文本编辑器两个实际的例子来讲解。此外,还会讲到它跟单例、缓存、对象池的区别和联系。在下一节课中,还会剖析一下享元模式在 Java IntegerString 中的应用。


享元模式原理与实现

所谓 “享元” ,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象时不可变对象

具体来讲,当一个系统中存在大量重复对象时,如果这些重复对象是不可变对象,就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多出代码引用。这样可以减少内存中对象的数量,起到节省内存的作用。实际上,不仅仅相同的对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分抽取出来,设计成享元,让这些大量相似对象引用这些享元。

不可变对象的指的是,一旦通过构造函数初始化完成后,它的状态(对象的成员变量或者属性)就不会被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。

之所以要求享元是不可变对象,那是因为它会被多出代码共享使用,避免一处代码对享元进行了修改,影响到其他的代码。

假设我们在开发一个棋牌游戏(比如象棋)。一个游戏厅中有成千上万个 “房间”,每个房间对应一个棋局。棋局要保证每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中, ChessPiece 类表示棋子,ChessBoard 表示一个棋局,里面保存了象棋中 32 个棋子的信息。

public class ChessPiece { // 棋子private int id;private String text;private Color color;private int positionX;private int positionY;public static enum Color {RED, BLACK}// 省略其他属性和getter/setter方法...
}public class ChessBoard {Map<Integer, ChessPiece> chessPieces = new HashMap<>();public ChessBoard() {this.init();}private void init() {chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 1));chessPieces.put(2, new ChessPiece(2, "馬", ChessPiece.Color.BLACK, 0, 1));// 省略摆放的其他棋子的代码...}public void move(int chessPieceId, int toPositionX, int toPositionY) {// ...}
}

为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。

这个时候,享元模式就派上用场了。像刚刚的实现方式在内存中会有大量的相似对象。这些对象的 idtextcolor 都是想通的,唯独 positionXpositionY 不同。实际上,我们可以将棋子的 idtextcolor 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录棋子的位置信息就可以了。具体的代码实现如下所示。

public class ChessPieceUnit {private int id;private String text;private Color color;public static enum Color {RED, BLACK}// 省略其他属性和getter方法...
}public class ChessPieceUnitFactory {private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();static {pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));pieces.put(2, new ChessPieceUnit(2, "馬", ChessPieceUnit.Color.BLACK));// 省略其他棋子的代码...}public static ChessPieceUnit getChessPieceUnit(int chessPieceId) {return pieces.get(chessPieceId);}
}public class ChessPiece { // 棋子private ChessPieceUnit chessPieceUnit;private int positionX;private int positionY;public ChessPiece(ChessPieceUnit chessPieceUnit, int positionX, int positionY) {this.chessPieceUnit = chessPieceUnit;this.positionX = positionX;this.positionY = positionY;}// 省略getter/setter方法
}public class ChessBoard {Map<Integer, ChessPiece> chessPieces = new HashMap<>();public ChessBoard() {this.init();}private void init() {chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPieceUnit(1), 0, 1));chessPieces.put(2, new ChessPiece(ChessPieceUnitFactory.getChessPieceUnit(2), 0, 1));// 省略摆放的其他棋子的代码...}public void move(int chessPieceId, int toPositionX, int toPositionY) {// ...}
}

在上面的代码实现中,我们利用工程类来缓存 ChessPieceUnit 信息。通过工厂类获取到的 ChessPieceUnit 就是享元。所有 ChessBoard 对象共享这 32 个 ChessPieceUnit 对象。在使用享元模式之前,记录 1 万个棋局,要创建 32 万个棋子的 ChessPieceUnit 对象。利用响应模式,只需要创建 32 享元对象供所有棋局共享使用,大大节省了内存。

那享元模式的原理讲完了,来总结下它的代码结构。实际上,它的代码结构非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。

享元模式在文本编辑器中的应用

弄懂了享元模式的原理和实现之后,再来看另一个例子,如何利用享元模式来优化文本编辑器的内存占用?

你可以把这里提到的文本编辑器想象成 Office 的 Word。不过,为了简化需求背景,假设这个文本编辑器只实现文字编辑功能,不包含图片、表格等复杂的编辑功能。对于简化之后的文本编辑器,我们在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中格式包括字体大小、颜色等信息。

尽管在实际的文档编写中,一般都是按照文本类型(标题、正文、…)来设置文字的格式。但是,从理论上讲,我们可以给文本文件中的每个文字都设置不同的格式。为了实现如此灵活的设置,并且代码实现又不过于复杂,我们把每个文字都当做一个独立对象来看待,并且在其中包含它的格式信息。具体代码如下所示:

public class Character {private char c;private Font font;private int size;private int colorRGB;public Character(char c, Font font, int size, int colorRGB) {this.c = c;this.font = font;this.size = size;this.colorRGB = colorRGB;}
}public class Editor {private List<Character> chars = new ArrayList<>();public void appendCharacter(char c, Font font, int size, int colorRGB) {Character character = new Character(c, font, size, colorRGB);chars.add(character);}
}

在文本编辑器中,每敲一个字,都会调用 Editor 类中的 appendCharacter() 方法,创建一个新的 Character 对象,保存到 chars 中。如果一个文件中,有上万、几十万的文字,那我们就要在内存中存储这么多的 Character 对象。

实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不大可能有人把每个文字都设置成不同的格式。所以,对于字体格式,我们可以将它设计成享元,让不同的文字共享使用。按照这个设计思路,对上面的代码进行重构。

public class CharacterStyle {private Font font;private int size;private int colorRGB;public CharacterStyle(Font font, int size, int colorRGB) {this.font = font;this.size = size;this.colorRGB = colorRGB;}@Overridepublic boolean equals(Object obj) {CharacterStyle otherStyle = (CharacterStyle) obj;return font.equals(otherStyle.font)&& size == otherStyle.size&& colorRGB == otherStyle.colorRGB;}
}public class CharacterStyleFactory {private static final List<CharacterStyle> styles = new ArrayList<>();public static CharacterStyle getStyle(Font font, int size, int colorRGB) {CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);for (CharacterStyle style : styles) {if (style.equals(newStyle)) {return style;}}styles.add(newStyle);return newStyle;}
}public class Character {private char c;private CharacterStyle style;public Character(char c, CharacterStyle style) {this.c = c;this.style = style;}
}public class Editor {private List<Character> chars = new ArrayList<>();public void appendCharacter(char c, Font font, int size, int colorRGB) {Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));chars.add(character);}
}

享元 VS 单例、缓存、对象池

上面的讲解中,多次提到 “共享” “缓存” “复用” 这些字眼,那它和单例、缓存、对象池这些概念有什么区别呢?

享元模式和单例的区别

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多出代码引用共享。实际上,享元模式有点类似与单例的变体:多例。

前面也多次提到,区别两种设计模式,不能光看代码,还要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例是为了限制对象的个数

享元模式和缓存的区别

  • 在享元模式的实现中,我们通过工厂类来缓存已经创建好的对象。这里的 “缓存” 实际上是 “存储” 的意思。
  • 跟我们平时所说的 “数据库缓存” “CPU缓存” “MemCache 缓存” 是两回事。我们平时将的缓存,主要是为了提高访问效率,而非复用。

享元模式和对象池的区别

对象池、连接池、线程池等也是为了复用,它们和享元模式有什么区别呢?

你可能对连接池、线程池比较熟悉,对对象池比较陌生,所以这里简单解释下。像 C++ 这样的编程语言,内存的管理是由程序员负责的。为了避免频繁地创建与释放导致内存碎片,我们可以预先申请一片连续的空间,也就是这里所说的对象池。每次创建对象时,我们从对象池中取出一个空闲对象来使用,对象使用完之后,再放回到对象池中以供后续复用,而非直接释放掉。

虽然对象池、连接池、线程池、享元模式都是为了复用,但是,如果我们在细致地扣一扣 “复用” 这个字眼的话,对象池、连接池、线程池 等池化技术中的 “复用” 和享元模式中的 “复用” 实际上是不同的概念。

  • 池化技术中的 “复用” 可以理解为重复使用,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
  • 享元模式中的 “复用” 可以理解为共享使用。在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间

总结

1.享元模式的原理

所谓 “享元”,就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象时不可变对象。具体来讲,当一个系统中存在大量重复对象的时候,就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以及起到节省内存的目的。

实际上,不仅仅相同的对象可以设计成享元,对于相似的对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引入这些享元。

2.享元模式的实现

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的对象,已达到复用的目的。

3.享元模式 VS 单例、缓存、对象池

  • 应用单例是为了保证对象全局唯一。
  • 应用享元模式是为了实现对象复用,节省内存。
  • 缓存是为了提高访问效率,而非复用。
  • 池化技术中的 “复用” 理解为 “重复使用”,主要是为了节省实现。

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

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

相关文章

iOS - Runtime-API

文章目录 iOS - Runtime-API1. Runtime应用1.1 字典转模型1.2 替换方法实现1.3 利用关联对象给分类添加属性1.4 利用消息转发机制&#xff0c;解决方法找不到的异常问题 2. Runtime-API2.1 Runtime API01 – 类2.1.1 动态创建一个类&#xff08;参数&#xff1a;父类&#xff0…

【Pt】马灯贴图绘制过程 02-制作锈迹

目录 一、边缘磨损效果 二、刮痕效果 三、边缘磨损与刮痕的混合 四、锈迹效果 本篇效果&#xff1a; 一、边缘磨损效果 将智能材质“Iron Forge Old” 拖入图层 打开“Iron Forge Old” 文件夹&#xff0c;选中“Sharpen”&#xff08;锐化&#xff09;&#xff0c;增大“…

2010-2021年银行网点及员工信息数据

2010-2021年银行网点及员工信息数据 1、时间&#xff1a;2010-2021年 2、来源&#xff1a;整理自csmar 3、指标&#xff1a;银行代码、股票代码、银行中文简称、统计截止日期、分行数量、机构网点数量、其中&#xff1a;境内网点数量、其中&#xff1a;境外网点数量、在职员…

Linux集群

目录 一、什么是集群&#xff1f; 二、 搭建(tomcatnginxkeepalived)集群 一、JDK安装 二、Tomcat安装 三、Nginx 3.1、什么是Nginx&#xff1f; 3.2、下载Nginx 3.3、安装 四、搭建NginxTomcat的实现集群 配置nginx.comf文件 五&#xff1a;Nginx搭建图片服务器 …

组件上使用 v-for

我们可以直接在组件上使用 v-for&#xff0c;和在一般的元素上使用没有区别 (别忘记提供一个 key)&#xff1a; <MyComponent v-for"item in items" :key"item.id" /> 但是&#xff0c;这不会自动将任何数据传递给组件&#xff0c;因为组件有自己独…

openGauss CM

CM 可获得性 本特性自openGauss 3.0.0版本开始引入。 特性简介 CM&#xff08;Cluster Manager&#xff09;是一款数据库管理软件&#xff0c;由cm_server和cm_agent组成。 cm_agent是部署在数据库每个主机上&#xff0c;用来启停和监控各个数据库实例进程的数据库管理组件…

【Java程序设计】【C00392】基于(JavaWeb)Springboot的校园生活服务平台(有论文)

基于&#xff08;JavaWeb&#xff09;Springboot的校园生活服务平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过…

C#面:程序集的主版本号和次版本号

C# 程序集的版本号和次版本号是程序集的一部分&#xff0c;用于标识程序集的不同版本。版本号通常由四个部分组成&#xff1a;主版本号、次版本号、生成号和修订号。其中&#xff0c;主版本号和次版本号是最常用的两个部分。 主版本号&#xff08;Major Version&#xff09;&a…

VUE 实现文件夹上传(保留目录结构)

代码&#xff1a;https://gitee.com/xproer/up6-vue-cli 1.引入up6组件 2.配置接口地址 接口地址分别对应&#xff1a;文件初始化&#xff0c;文件数据上传&#xff0c;文件进度&#xff0c;文件上传完毕&#xff0c;文件删除&#xff0c;文件夹初始化&#xff0c;文件夹删除&…

2024年做视频号小店是不是明智之举?这篇文章告诉你答案

大家好&#xff0c;我是电商糖果 视频号自从去年电商的知名度打开之后&#xff0c;不少朋友都盯上这块肥肉。 要知道现在可是短视频电商的时代&#xff0c;抖音&#xff0c;快手靠做电商赚了不少钱。 视频号又怎么会放过这次的风口呢&#xff1f; 也有不少想做电商的朋友问…

灯哥驱动器端口讲解----foc电机驱动必看

CS:是电流采样的引脚&#xff0c;三项采样电流&#xff0c;现在只给了两路&#xff0c;另外一路算出来就行了 in:三项电流输入&#xff0c;驱动电机使用。 en:没有用 SDA,SCL&#xff1a;I2C的引脚用来读取编码器的计数值 tx,rx&#xff1a;引出来了一路串口&#xff0c;没有用…

西安 专业nft开发NFT寄售-NFT抢购-NFT盲盒-NFT空投

在数字化时代的今天&#xff0c;非同质化代币&#xff08;NFT&#xff09;正成为数字资产领域的新宠。作为区块链技术的一种应用&#xff0c;NFT 赋予数字资产独一无二的身份和价值&#xff0c;从而在艺术、游戏、音乐、收藏品等领域掀起了一股热潮。西安&#xff0c;这座千年古…

LeetCode-热题100:42. 接雨水

题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a; height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a; 6 解释&#xff1a; 上面是由数组 [0,1,0,2,1,…

java 设计模式详解(持续更新)

单例模式 观察者模式 装饰者模式 适配器模式 工厂模式 代理模式 单例模式 简单点说&#xff0c;就是一个应用程序中&#xff0c;某个类的实例对象只有一个&#xff0c;你没办法去new&#xff0c;因为构造器是被private修饰的&#xff0c;一般通过getInstance()的方法来获取…

2024社工考试报名详细流程来啦✅

2024社工考试报名详细流程来啦✅ ⏰社工报名时间&#xff1a;4月1日-4月18日 &#x1f447;&#x1f3fb;2024年社工报名流程 1、打开人事考试网&#xff0c;点击左侧【网上报名】 2、没有用户名的点击新用户注册&#xff0c;有用户名的直接输入用户名密码登录即可。 3、注册好…

Day26 HashMap

Day26 HashMap 文章目录 Day26 HashMap一、应用场景二、特点三、基本用法四、面试题 一、应用场景 1、概念&#xff1a; HashMap是Java集合框架中的一种实现类&#xff0c;用于存储键值对。 2、好处&#xff1a; HashMap是一个常用的集合类&#xff0c;适用于需要快速查找和插…

24计算机考研调剂 | 【官方】北京科技大学

北京科技大学 考研调剂招生信息 招生专业&#xff1a; 085404&#xff08;计算机技术&#xff09; 081200&#xff08;计算机科学与技术&#xff09; 调剂要求&#xff1a;&#xff08;调剂基本分数&#xff09; 我中心将在教育部“全国硕士生招生调剂服务系统”&#xff08…

postgres12.4安装pg_rman-1.3.16

操作系统版本&#xff1a;centos7.6 X64 pg_rman版本&#xff1a;pg_rman-1.3.16-pg12.tar.gz postgres版本&#xff1a;postgresql-12.4.tar.gz 备份文件存放路径&#xff1a;/home/postgres/backup 归档日志存放路径&#xff1a;/home/postgres/archivelog/ postgres用户的环…

It takes two (搜索)

本题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 题目&#xff1a; 样例&#xff1a; 输入 3 4 AAAO AAAA AAAA 输出 NO 思路&#xff1a; 根据题目意思&#xff0c;如果存在的 A 联通不可以成为 矩形&#xff0c;输出 NO&#xff0c;否则输出 YES 这道题看数据范…

网络套接字补充——UDP网络编程

五、UDP网络编程 ​ 1.对于服务器使用智能指针维护生命周期&#xff1b;2.创建UDP套接字&#xff1b;3.绑定端口号&#xff0c;包括设置服务器端口号和IP地址&#xff0c;端口号一般是2字节使用uint16_t&#xff0c;而IP地址用户习惯使用点分十进制格式所以传入的是string类型…