JavaScript设计模式之享元模式

692 阅读5分钟

本文由我们团队肖建朋总结

最近开发由于项目越来越大,发现项目中需要优化的地方越来越多,就抽空看看了享元模式。

享元模式(flyweight)是一种用于性能优化的模式,“fly”在这里指苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

初识享元模式

看概念可能会有些懵逼,那么我看先来看个例子:有个服装厂,生产了男女服装各50种款式,为了推销需要找模特来拍照,正常可能会找男女模特各50个,每人穿一种拍一组照片。这时用程序实现就会这样写:

var Model = function(sex, clothes) {
    this.sex = sex
    this.clothes = clothes
    this.takePhoto = function() {
        console.log( 'sex= ' + this.sex + ' clothes=' + this.clothes);
    }
}
let iM = 1
while(iM <= 50){
    var model = new Model('男', 'clothes' + iM)
    model.takePhoto()
    iM++
}
let iF = 1
while(iF <= 50){
    var model = new Model('女', 'clothes' + iF)
    model.takePhoto()
    iF++
}

以上代码可以看出需要创建100个model对象,随着衣服种类的增多,就需要创建更多的对象,当对象多到一定数量程序必然会崩溃。

下面我们就用享元模式来优化一下这段代码,首先我们可以发现一个男模特是可以穿50种男式衣服的任意一件的。同样女模特可以穿50种女式衣服任意一件。所以对于服装厂来说只需要找一个男模特和一个女模特就可以了。

改造后的代码如下:

var Model = function(sex) {
    this.sex = sex
    this.takePhoto = function() {
        console.log( 'sex= ' + this.sex + ' clothes=' + this.clothes);
    }
}
var mModel = new Model('男')
var fModel = new Model('女')
var i = 1
while(i <= 50) {
    mModel.clothes = ' clothes=男式衣服=' + i
    mModel.takePhoto()
    fModel.clothes = ' clothes=女式衣服=' + i
    fModel.takePhoto()
    i++
}

这里我们只创建一个男模特对象和一个女模特对象,然后让他们一次试穿拍照男女的50套衣服,代码是不是清爽了许多,而且空间复杂对也降低了。

享元模式的内部状态和外部状态

享元模式要求将对象的属性划分为内部状态与外部状态(这里的状态通常指属性)。享元模式的目标是尽量减少共享对象的数量。

划分内部状态和外部状态的可以从以下几点来区分:

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

根据上面几点我们可以看出上一个例子中,sex就是Model的内部状态,clothes就是它的外部状态

由于组装外部状态成为一个完整对象的过程需要花费一定的时间,但是可以大大减少系统中的对象数量,因此享元模式是一种用时间换空间的优化模式

享元模式的通用结构

上面衣服和模特的例子,初步展示了享元模式的威力,但这并不是一个完整的享元模式,因为存在以下问题:

  1. 模特对象在其他场景中可能并不需要一开始就创建所有共享对象。
  2. 外部状态在其他场景中可能会非常复杂,这样他们与共享对象的联系就会变得很困难。

解决了这两个问题,就可以得出享元模式的通用结构:

  1. 工厂模式创建共享对象,当共享对象被真正需要时,它才从工厂中被创建出来。
  2. 管理器记录外部状态,使用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

基于上面总结的享元模式通用结构,我们来实现一个完整的享元模式例子:文件上传

文件上传

// 构造Upload 对象,包含内部状态 uploadType 和一个delFile方法
var Upload = function( uploadType){
    this.uploadType = uploadType;

    this.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 );
        }
    }
}
// 构造上传工厂来创建upload对象
var UploadFactory = (function () {
    var createdFlyWeightObjs = {};
    return {
        create: function (uploadType) {
            if (createdFlyWeightObjs[uploadType]) {
                return createdFlyWeightObjs[uploadType];
            }
            return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
        }
    }
})();
// 上传管理器,负责向UploadFactory提交对象创建申请,并保存所有upload对象的外部状态到uploadDatabase对象中,
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];
            }
        }
    }
})();
var id = 0;
// startUpload上传函数,调用uploadManager
window.startUpload = function (uploadType, files) {
    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
    }
]);

享元模式的使用场景

  1. 一个程序中使用了大量的相似对象。
  2. 由于使用了大量对象,造成内存开销过大。
  3. 对象的大多数状态都可以变为外部状态。
  4. 剥离出对象的外部状态后,可以用较少的共享对象取代大量对象。

总结

享元模式是一种为了解决性能问题而生的模式,在一个存在大量相似对象的系统中,享元模式可以很好的解决大量对象带来的性能问题。