企业网站建设方案书怎么写,环县网站怎么做,网站实现搜索功能,海南在线人才网招聘官网参考资料
曾探《JavaScript设计模式与开发实践》#xff1b;「设计模式 JavaScript 描述」享元模式设计模式之享元模式Javascript 设计模式 - 享元模式
定义
享元模式的英文叫#xff1a;Flyweight Design Pattern。享元设计模式是用于性能优化的模式#xff0c;这种设计…参考资料
曾探《JavaScript设计模式与开发实践》「设计模式 JavaScript 描述」享元模式设计模式之享元模式Javascript 设计模式 - 享元模式
定义
享元模式的英文叫Flyweight Design Pattern。享元设计模式是用于性能优化的模式这种设计模式的核心在于可以共享技术并支持对大量细分过后的对象进行调整如果系统中因为创建大量类似的对象而导致内存占用过高享元设计模式在其中就会起到非常重要的作用因为它可以使其减少重复创建相同类似的实例对象。在JavaScript中浏览器特别是移动端的浏览器部分所能够使用的内存并不是很多所以在其中节省内存就变得至关重要。
享元模式属于结构型模式它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式Flyweight Pattern主要用于减少创建对象的数量以减少内存占用和提高性能。
享就是分享之意指一物被众人共享而这也正是该模式的终旨所在元意为单元蝇量级的个体该模式的核心就是使用共享技术来有效的支持大量的细粒度对象。
使用场景
数据库连接池线程池String常量池
内部状态与外部状态
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量那么如何划分内部状态和外部状态呢
内部状态存储于对象内部。内部状态可以被一些对象共享。内部状态独立于具体的场景通常不会改变。外部状态取决于具体的场景并根据场景而变化外部状态不能被共享。
把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态 可以从对象身上剥离出来并储存在外部。
剥离了外部状态的对象成为共享对象外部状态在必要时被传入共享对象来组装成一个完整 的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间但却可以大大减少系 统中的对象数量相比之下这点时间或许是微不足道的。因此享元模式是一种用时间换空间的优化模式。
使用享元模式的关键是如何区别内部状态和外部状态。
举例说明-下象棋
我们还是通过网络上开房间下象棋的经典案例来说明享元设计模式。一个象棋游戏平台肯定有很多的房间每个房间都有一盘棋每盘棋上有32颗棋子就有32个对象但是每个房间里的【将】的属性有很多都是相同的比如名字、颜色等而且它们在各个房间的棋盘中都是不会发生变化的唯一不同的就是所在的位置不一样。所以相同的属性就可以抽出来作为共享单元。
看例子
享元类即棋子公共的属性
public class ShareChessAttr {private Integer id;private String name;private String color;public ShareChessAttr(Integer id, String name, String color) {this.id id;this.name name;this.color color;}
}定义一个工厂去获取对应棋子的享元
public class ShareChessAttrFactory {public static final HashMapInteger, ShareChessAttr shareChessAttrMap new HashMap();static {shareChessAttrMap.put(1, new ShareChessAttr(1, 将, 红));shareChessAttrMap.put(2, new ShareChessAttr(2, 帅, 黑));shareChessAttrMap.put(3, new ShareChessAttr(3, 车, 红));shareChessAttrMap.put(4, new ShareChessAttr(4, 车, 黑));}public static ShareChessAttr getShareChessAttr(Integer i) {return shareChessAttrMap.get(i);}
}定义棋子
public class Chess {private ShareChessAttr shareChessAttr;private Integer positionX;private Integer positionY;public Chess(ShareChessAttr shareChessAttr, Integer positionX, Integer positionY) {this.shareChessAttr shareChessAttr;this.positionX positionX;this.positionY positionY;}
}定义棋盘
public class ChessBoard {private HashMapInteger, Chess chessOnBoard new HashMapInteger, Chess();public ChessBoard() {chessOnBoard.put(1, new Chess(ShareChessAttrFactory.getShareChessAttr(1), 5, 0));chessOnBoard.put(2, new Chess(ShareChessAttrFactory.getShareChessAttr(2), 5, 0));}
}如此每个棋盘中的棋子的id名字和颜色都指向了同一个shareChessAttr。实现享元。
举例说明- HashMap 对象池
代码关键点用 HashMap 对象池存储这些对象。
// 享元模式对象池缓存对象
class colorFactory {constructor(name) {this.colors {};}create(name) {let color this.colors[name];if (color) return color;this.colors[name] new Color(name);return this.colors[name];}
};举例说明- 文件上传
在云文件上传模块的开发中我们可以借助享元模式提升了程序的性能。下面我们就讲述这个例子。
对象爆炸
在云文件上传模块的开发中可能会出现对象爆炸的问题。云文件的文件上传功能虽然可以选择依照队列一个一个地排队上传但也支持同时选择 2000 个文件。每一个文件都对应着一个 JavaScript 上传对象的创建可是往程序里同时 new 了 2000 个 upload 对象结 果可想而知Chrome 中还勉强能够支撑IE 下直接进入假死状态。
云文件支持好几种上传方式比如浏览器插件、Flash 和表单上传等为了简化例子我们先假设只有插件和 Flash 这两种。不论是插件上传还是 Flash 上传原理都是一样的当用户选择了文件之后插件和 Flash 都会通知调用 Window 下的一个全局 JavaScript 函数它的名字是 startUpload用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里代码如下
let id 0;window.startUpload function (uploadType, files) { // uploadType 区分是控件还是 flash for (let i 0; i files.length; i) {const uploadObj new Upload(uploadType, files[i].fileName, files[i].fileSize);uploadObj.init(id); // 给 upload 对象设置一个唯一的 id }
};当用户选择完文件之后startUpload 函数会遍历 files 数组来创建对应的 upload 对象。接下来定义 Upload 构造函数它接受 3 个参数分别是插件类型、文件名和文件大小。这些信息都已经被插件组装在 files 数组里返回代码如下
const Upload function (uploadType, fileName, fileSize) {this.uploadType uploadType;this.fileName fileName;this.fileSize fileSize;this.dom null;
};Upload.prototype.init function (id) {const that this;this.id id;this.dom document.createElement(div);this.dom.innerHTML span文件名称: this.fileName , 文件大小: this.fileSize /span button classdelFile删除/button;this.dom.querySelector(.delFile).onclick function () {that.delFile();}document.body.appendChild(this.dom);
};同样为了简化示例我们暂且去掉了 upload 对象的其他功能只保留删除文件的功能对应 的方法是 Upload.prototype.delFile。该方法中有一个逻辑当被删除的文件小于 3000 KB 时该文件将被直接删除。否则页面中会弹出一个提示框提示用户是否确认要删除该文件代码如下
Upload.prototype.delFile function () {if (this.fileSize 3000) {return this.dom.parentNode.removeChild(this.dom);}if (window.confirm(确定要删除该文件吗? this.fileName)) {return this.dom.parentNode.removeChild(this.dom);}
};接下来分别创建 3 个插件上传对象和 3 个 Flash 上传对象
startUpload(plugin, [{fileName: 1.txt,fileSize: 1000},{fileName: 2.html,fileSize: 3000},{fileName: 3.txt,fileSize: 5000}
]);startUpload(flash, [{fileName: 4.txt,fileSize: 1000},{fileName: 5.html,fileSize: 3000},{fileName: 6.txt,fileSize: 5000}
]);当点击删除最后一个文件时可以看到弹出了是否确认删除的提示如下图所示。
享元模式重构文件上传
上一节的代码是第一版的文件上传在这段代码里有多少个需要上传的文件就一共创建了多少个 upload 对象接下来我们用享元模式重构它。
首先我们需要确认插件类型 uploadType 是内部状态那为什么单单 uploadType 是内部状态呢前面讲过划分内部状态和外部状态的关键主要有以下几点。
内部状态储存于对象内部。内部状态可以被一些对象共享。内部状态独立于具体的场景通常不会改变。外部状态取决于具体的场景并根据场景而变化外部状态不能被共享。
在文件上传的例子里upload 对象必须依赖 uploadType 属性才能工作这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别它们各自调用的接口也是完全不一样的必须在对象创建之初就明确它是什么类型的插件才可以在程序的运行过程中让它们分别调用各自的 start、pause、cancel、del 等方法。
实际上在云文件的真实代码中虽然插件和 Flash 上传对象最终创建自一个大的工厂类但它们实际上根据 uploadType 值的不同分别是来自于两个不同类的对象。在目前的例子中为了 简化代码我们把插件和 Flash 的构造函数合并成了一个。
一旦明确了 uploadType无论我们使用什么方式上传这个上传对象都是可以被任何文件共 用的。而 fileName 和 fileSize 是根据场景而变化的每个文件的 fileName 和 fileSize 都不一样fileName 和 fileSize 没有办法被共享它们只能被划分为外部状态。
剥离外部状态
明确了 uploadType 作为内部状态之后我们再把其他的外部状态从构造函数中抽离出来Upload 构造函数中只保留 uploadType 参数
const Upload function (uploadType) {this.uploadType uploadType;
};Upload.prototype.init 函数也不再需要因为 upload 对象初始化的工作被放在了 uploadManager.add 函数里面接下来只需要定义 Upload.prototype.del 函数即可
Upload.prototype.delFile function (id) {uploadManager.setExternalState(id, this); // (1) if (this.fileSize 3000) {return this.dom.parentNode.removeChild(this.dom);}if (window.confirm(确定要删除该文件吗? this.fileName)) {return this.dom.parentNode.removeChild(this.dom);}
};在开始删除文件之前需要读取文件的实际大小而文件的实际大小被储存在外部管理器 uploadManager 中所以在这里需要通过 uploadManager.setExternalState 方法给共享对象设置正确的 fileSize上段代码中的(1)处表示把当前 id 对应的对象的外部状态都组装到共享对象中。
工厂进行对象实例化
接下来定义一个工厂来创建 upload 对象如果某种内部状态对应的共享对象已经被创建过那么直接返回这个对象否则创建一个新的对象
const UploadFactory (function () {const createdFlyWeightObjs {};return {create: function (uploadType) {if (createdFlyWeightObjs[uploadType]) {return createdFlyWeightObjs[uploadType];}return createdFlyWeightObjs[uploadType] new Upload(uploadType);}}
})();管理器封装外部状态
现在我们来完善前面提到的 uploadManager 对象它负责向 UploadFactory 提交创建对象的请求并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态以便在程序运行过程中给 upload 共享对象设置外部状态代码如下
const uploadManager (function () {const uploadDatabase {};return {add: function (id, uploadType, fileName, fileSize) {const flyWeightObj UploadFactory.create(uploadType);const dom document.createElement(div);dom.innerHTML span文件名称: fileName , 文件大小: fileSize /span button classdelFile删除/button;dom.querySelector(.delFile).onclick function () {flyWeightObj.delFile(id);}document.body.appendChild(dom);uploadDatabase[id] {fileName: fileName,fileSize: fileSize,dom: dom};return flyWeightObj;},setExternalState: function (id, flyWeightObj) {const uploadData uploadDatabase[id];for (const i in uploadData) {flyWeightObj[i] uploadData[i];}}}
})();然后是开始触发上传动作的 startUpload 函数
let id 0;
window.startUpload function (uploadType, files) {for (let i 0; i files.length; i) {const uploadObj uploadManager.add(id, uploadType, files[i].fileName, files[i].fileSize);}
};最后是测试时间运行下面的代码后可以发现运行结果跟用享元模式重构之前一致
startUpload(plugin, [{fileName: 1.txt,fileSize: 1000},{fileName: 2.html,fileSize: 3000},{fileName: 3.txt,fileSize: 5000}
]);startUpload(flash, [{fileName: 4.txt,fileSize: 1000},{fileName: 5.html,fileSize: 3000},{fileName: 6.txt,fileSize: 5000}
]);享元模式重构之前的代码里一共创建了 6个 upload 对象而通过享元模式重构之后对象的数量减少为 2更幸运的是 就算现在同时上传 2000个文件需要创建的 upload 对象数量依然是 2。
享元模式的适用性
享元模式是一种很好的性能优化方案但它也会带来一些复杂性的问题从前面两组代码的比较可以看到使用了享元模式之后我们需要分别多维护一个 factory 对象和一个 manager 对 象在大部分不必要使用享元模式的环境下这些开销是可以避免的。
享元模式带来的好处很大程度上取决于如何使用以及何时使用一般来说以下情况发生时便可以使用享元模式。
一个程序中使用了大量的相似对象。由于使用了大量对象造成很大的内存开销。对象的大多数状态都可以变为外部状态。剥离出对象的外部状态之后可以用相对较少的共享对象取代大量对象。
可以看到文件上传的例子完全符合这四点。
再谈内部状态和外部状态
如果顺利的话通过前面的例子我们已经了解了内部状态和外部状态的概念以及享元模式的工作原理。我们知道实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合系统中便最多存在多少个共享对象而外部状态储存在共享对象的外部在必要时被传入共享对象来组装成一个完整的对象。现在来考虑两种极端的情况即对象没有外部状态和没有内部状态的时候。
没有内部状态的享元
在文件上传的例子中我们分别进行过插件调用和 Flash 调用即 startUpload(plugin, []) 和 startUpload(flash, [])导致程序中创建了内部状态不同的两个共享对象。也许你会奇怪在文件上传程序里一般都会提前通过特性检测来选择一种上传方式如果浏览器支持插件就用插件上传如果不支持插件就用 Flash 上传。那么什么情况下既需要插件上传又需要 Flash 上传呢
实际上这个需求是存在的很多网盘都提供了极速上传控件与普通上传Flash两种模式如果极速上传不好使可能是没有安装控件或者控件损坏用户还可以随时切换到普通上传模式所以这里确实是需要同时存在两个不同的 upload 共享对象。
但不是每个网站都必须做得如此复杂很多小一些的网站就只支持单一的上传方式。假设我们是这个网站的开发者不需要考虑极速上传与普通上传之间的切换这意味着在之前的代码中作为内部状态的 uploadType 属性是可以删除掉的。 在继续使用享元模式的前提下构造函数 Upload 就变成了无参数的形式
const Upload function(){}; 其他属性如 fileName、fileSize、dom 依然可以作为外部状态保存在共享对象外部。在 uploadType 作为内部状态的时候它可能为控件也可能为 Flash所以当时最多可以组合出两个共享对象。而现在已经没有了内部状态这意味着只需要唯一的一个共享对象。现在我们要改写创建享元对象的工厂代码如下
const UploadFactory (function () {let uploadObj;return {create: function () {if (uploadObj) {return uploadObj;}return uploadObj new Upload();}}
})();管理器部分的代码不需要改动还是负责剥离和组装外部状态。可以看到当对象没有内部状态的时候生产共享对象的工厂实际上变成了一个单例工厂。虽然这时候的共享对象没有内部状态的区分但还是有剥离外部状态的过程我们依然倾向于称之为享元模式。
没有外部状态的享元
网上许多资料中经常把 Java 或者 C#的字符串看成享元这种说法是否正确呢我们看看下面这段 Java 代码来分析一下
// Java 代码
public class Test {public static void main(String args[]) {String a1 new String(a).intern();String a2 new String(a).intern();System.out.println(a1 a2); // true }
}在这段 Java 代码里分别 new 了两个字符串对象 a1 和 a2。intern 是一种对象池技术 new String(a).intern()的含义如下。
如果值为 a 的字符串对象已经存在于对象池中则返回这个对象的引用。反之将字符串 a 的对象添加进对象池并返回这个对象的引用。
所以 a1 a2 的结果是 true但这并不是使用了享元模式的结果享元模式的关键是区别内部状态和外部状态。享元模式的过程是剥离外部状态并把外部状态保存在其他地方在合适的时刻再把外部状态组装进共享对象。这里并没有剥离外部状态的过程a1 和 a2 指向的完全就是同一个对象所以如果没有外部状态的分离即使这里使用了共享的技术但并不是一个纯粹的享元模式。
优缺点
优点
大大减少对象的创建降低系统的内存使效率提高。
缺点
提高了系统的复杂度需要分离出外部状态和内部状态而且外部状态具有固有化的性质不应该随着内部状态的变化而变化否则会造成系统的混乱。
总结
享元模式是为解决性能问题而生的模式这跟大部分模式的诞生原因都不一样。在一个存在 大量相似对象的系统中享元模式可以很好地解决大量对象带来的性能问题。
享元模式的思想与单例比较类似但是单例模式强调的是全局唯一而享元模式则强调的是内存共享。