设计模式之-享元模式

享元模式是一种用于性能优化的模式。
核心是运用共享技术来有效支持大量细粒度的对象。

场景1:假设有个内衣工厂,目前产品有50个男式内衣和50个女式内衣,为了推销产品,需要给这些产品生产一些塑料模特来穿上他们的内衣拍成广告。

来看一个例子,不使用享元模式的情况下:

javascript 复制代码
 		var Model = function(sex,underwear){
            this.sex=sex;
            this.underwear = underwear;
        }
        Model.prototype.takePhoto = function(){
            console.log(`sex=${this.sex} underwear=${this.underwear}`);
        }
        for(var i=1;i<=50;i++){
            var maleModel = new Model('male', `underwear${i}`);
            maleModel.takePhoto();
        }
        for(var j=1;j<=50;j++){
            var femaleModel=new Model('female',`underwear${j}`);
            femaleModel.takePhoto();
        }

要得到一张照片,每次需要传入sex和uunderwear参数,综上所述,现在一共有50种男内衣和50种女内衣,所以会产生100个对象,如果将来生产10000种内衣,那这个程序可能因为存在如此多对象已经提前崩溃。

思考下如何优化这个场景,虽然有100种内衣,但很显然不需要50个男模特和50个女模特,其实男女模特只需要各自有一个就行了,他们可以分别穿上不同内衣来拍照。下面来改写下这个代码

javascript 复制代码
var Model = function(sex,underwear){
            this.sex=sex;
            // this.underwear = underwear;
        }
        Model.prototype.takePhoto = function(){
            console.log(`sex=${this.sex} underwear=${this.underwear}`);
        }
        var maleModel = new Model('male');
        var femaleModel = new Model('female');

        for(var i=1;i<=50;i++){
            maleModel.underwear=`underwear${i}`
            maleModel.takePhoto();
        }
        for(var j=1;j<=50;j++){
            femaleModel.underwear = `underwear${j}`
            femaleModel.takePhoto();
        }

可以看到,改进之后的代码,只需要两个对象便完成了同样的功能。

这个例子就是享元模式的雏形,享元模式要求将对象的属性划分为内部状态和外部状态(状态在这里通常指属性),享元模式的目标是尽量减少共享对象的数量。

  1. 内部状态存储于对象内部
  2. 内部状态可以被一些对象共享。
  3. 内部状态独立于具体的场景,通常不会改变。
  4. 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

在上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系统中的对象数量,通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女,所以该内衣厂商最多只需要2个对象。

这样一来,我们便可以把所有内部状态相同对象指定为同一个共享对象,而外部状态可以从对象身上剥离出来,并存储在外部。

剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象,虽然组装外部状态成为一个完整对象的过程需要花费一定事件,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。

下面看下这个例子

javascript 复制代码
		var id=0;
        window.startUpload= function(uploadType,files){ // uploadType是区分控件还是flash
            for(var i=0,file;file=files[i++];){
                var uploadObj = new Upload(uploadType,file.fileName,file.fileSize);
                uploadObj.init(id++); // 给upload对象设置一个唯一的id
            }
        }
        var Upload = function(uploadType,fileName,fileSize){
            this.uploadType=uploadType;
            this.fileName = fileName;
            this.fileSize=fileSize;
            this.dom=null;
        }
        Upload.prototype.init = function(id){
            var that=this;
            this.id=id;
            this.dom=document.createElement('div');
            this.dom.innerHTML = `<span>文件名称:${this.fileName},文件大小:${this.fileSize}</span>
            <button class="delFile">删除</button>
            `;
            this.dom.querySelector('.delFile').onclick=function(){
                that.delFile();
            }
            document.body.appendChild(this.dom);
        }
        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);
            }
        }

        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对象,接下来我们用享元模式重构他

javascript 复制代码
		// 剥离外部状态
        var Upload = function(uploadType,fileName,fileSize){
            this.uploadType=uploadType;
        }
        Upload.prototype.delFile = function(id){
            uploadManager.setExternalState(id,this);
            console.log(this)
            if(this.fileSize<3000){
                return this.dom.parentNode.removeChild(this.dom);
            }
            if(window.confirm('确定要删除该文件吗?'+this.fileName)){
                return this.dom.parentNode.removeChild(this.dom);
            }
        }
        // 工厂进行对象实例化,如果某个内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新对象
        var UploadFactory=(function(){
            var createFlyWeightObjs={};
            return {
                create:function(uploadType){
                    console.log(createFlyWeightObjs[uploadType],createFlyWeightObjs,uploadType)
                    if(createFlyWeightObjs[uploadType]){
                        return createFlyWeightObjs[uploadType];
                    }
                    return createFlyWeightObjs[uploadType] = new Upload(uploadType);
                }
            }
        })();
        // 管理器封装外部状态
        var uploadManager = (function(){
            var uploadDatabase = {};
            return {
                add:function(id,uploadType,fileName,fileSize){
                    var flyWeightObj = UploadFactory.create(uploadType);
                    var dom = document.createElement('div');
                    dom.innerHTML = `<span>文件名称:${fileName},文件大小:${fileSize}</span>
                    <button class="delFile">删除</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){
                    var uploadData = uploadDatabase[id];
                    for(var i in uploadData){
                        flyWeightObj[i]=uploadData[i];
                    }
                }
            }
        })();
        // 然后是开始出发上传动作的startUpload函数
        var id=0;
        window.startUpload= function(uploadType,files){ // uploadType是区分控件还是flash
            for(var i=0,file;file=files[i++];){
                var uploadObj = uploadManager.add(++id,uploadType,file.fileName,file.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。

没有内部状态的享元

在文件上传的例子中,我们分别进行过插件调用和Flash调用,即startUpload('plugin',[])和startUpload('flash',[]),导致程序中创建了内部状态不同的两个共享对象,也许你会奇怪,在文件上传程序里,一般都会提前通过特性检测来选择一种上传方式,如果浏览器支持插件就用插件,如果不支持插件,那就用flash上传,那么什么情况下既需要插件上传又需要Flash上传呢?

实际上这个需求是存在的,很多网盘都提供了极速上传(控件)与普通上传(Flash)两种模式,如果极速上传不好使(可能没有安装控件或者控件损坏),用户还可以随时切换到普通上传模式,这里确实需要同时存在两个不同的upload共享对象。

但并不是每个网站都必须做的如此复杂,很多小一些的网站就支持单一的上传方式,假设我们是这个网站的开发中,不需要考虑极速上传与普通上传之间的切换,这意味着在之前的代码中作为内部状态的uploadType属性是可以删除的

在继续使用享元模式的前提下,构造函数Upload就变成了无参数的形式。

javascript 复制代码
var Upload = function(){}

其他属性如fileName、fileSize,dom依然可以作为外部状态保存在共享对象外部,在uploadType作为内部状态的时候,他可能为控件,也可能为Flash,所以当时最多可以组合出两个共享对象。而现在已经没有了内部状态,这意味着只需要唯一的一个共享对象。现在我们要改写创建享元对象的工厂,代码如下:

javascript 复制代码
	var UploadFactory=(function(){
            var uploadObj;
            return {
                create:function(){
                    if(uploadObj){
                        return uploadObj;
                    }
                    return cuploadObj = new Upload();
                }
            }
        })();

管理器部分的代码不需要改动,还是负责剥离和组装外部状态,可以看到,当对象没有内部状态的时候,生产共享对象的工厂实际上变成了一个单例工厂。虽然这时候的共享对象没有了内部状态的区分,但还是有剥离外部状态的过程,我们依然倾向于称之为享元模式。

非原创,来源javascript设计模式与开发实践 -曾探

相关推荐
dly_blog2 小时前
ref 与 reactive 的本质区别(第3节)
前端·javascript·vue.js
D_C_tyu10 小时前
Vue3 + Element Plus | el-table 表格获取排序后的数据
javascript·vue.js·elementui
天外天-亮11 小时前
v-if、v-show、display: none、visibility: hidden区别
前端·javascript·html
沿着路走到底12 小时前
JS事件循环
java·前端·javascript
子春一213 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
jlspcsdn14 小时前
20251222项目练习
前端·javascript·html
拉不动的猪16 小时前
回顾计算属性的缓存与监听的触发返回结果
前端·javascript·vue.js
树欲静而风不止慢一点吧16 小时前
小米手环9应用/游戏开发快速入门
前端·javascript·小程序