设计模式之--享元模式

437 阅读7分钟

1.定义

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

如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了

2.初识享元模式

有个内衣工厂,产品有50种男士内衣和50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们分别穿上一件内衣来拍照。不用享元模式的情况下,代码如下:

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和underwear参数,如上所述,现在一共有50种男内衣和50种女内衣,所以一共会产生100个对象。如果将来生产了10000种内衣,那程序可能会因为存在如此多的对象而崩溃

优化下这个场景。虽然有100种内衣,但很明显并不需要50个男模特和50个女模特。其实男模特和女模特各自有一个就可以了,他们分别穿上不同的内衣来拍照。

既然只需要区别男女模特,那先把underwear参数从构造函数中移除,构造函数只接收sex参数

var Model = function(sex){
    this.sex = sex;
}

Model.prototype.takePhoto = function(){
     console.log('sex=' + this.sex + ' underwear=' + this.underwear);
}

var maleModel = new Model('male'),
    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();
}

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

3. 内部状态与外部状态

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

如何划分内部状态与外部状态

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

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

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

使用享元模式的关键是如何区分内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态。而外部状态取决于具体的场景,并根据场景变化而变化。

4. 享元模式的通用结构

以上的例子存在两个问题

  • 通过构造函数显式new出了男女两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象

  • 给model对象手动设置了underwear外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难

通过一个对象工厂来解决第一个问题,只有当某种共享对象被真正需要时,它才从工厂中创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来

5. 文件上传

5.1 对象爆炸

var id = 0;

window.startUpload = function( uploadType, files){
    for(var i=0, file; file = files[i++];) {
        var uploadObj = new Upload(uploadType, file.fileName, file.fizeSize);
        uploadObj.init(id++)
    }
}

var Upload = function(uploadType,fileName, fizeSize) {
    this.uploadType = uploadType;
    this.fileName = fileName;
    this.fizeSize = fizeSize;
    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(){
     return this.dom.parentNode.removeChild(this.dom);
}

startUpload('plugin',[
    {
        fileName: '1.txt',
        fileSize: 1000
    },
    {
        fileName: '2.html',
        fileSize: 2000
    }
])

startUpload('flash',[
    {
        fileName: '3.txt',
        fileSize: 1000
    },
    {
        fileName: '4.html',
        fileSize: 2000
    }
])

5.2 享元模式重构文件上传

上面的代码里,有多少个需要上传的文件,就一共创建了多少个upload对象,用享元模式重构它

首先,需要确认插件类型uploadType是内部状态,为什么单单uploadType是内部状态呢?划分内部状态和外部状态的关键主要有以下几点

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

一旦明确了 uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何对象共用的。而fileName和fileSize是根据场景而变化的,每个文件的fileName和fileSize都不一样,fileName和fileSize没有办法被共享,它们只能被划分为外部状态

5.3 剥离外部状态

明确了 uploadType 作为内部状态后,再把其他的外部状态从构造函数中抽离出来,Upload构造函数中只保留 uploadType参数

var Upload = function(uploadType){
    this.uploadType = uploadType
}

Upload.prototype.init函数也不再需要,因为upload对象初始化的工作被放在了uploadManager.add函数里面,接下来只需要定义Upload.prototype.del函数即可:

Upload.prototype.delFile = function(){
     return this.dom.parentNode.removeChild(this.dom);
}

5.4 工厂进行对象实例化

定义一个工厂来创建upload对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象

var UploadFactory = (function(){
    var createdFlyweightObjs = {}
    
    return {
        create: function(uploadType) {
            if (createdFlyweightObjs[uploadType]) {
                return createdFlyweightObjs[uploadType]
            }
            
            return createdFlyweightObjs[uploadType] = new Upload(uploadType)
        }
    }
})();

5.5 管理器封装外部状态

前面提到的uploadManager对象, 它负责向UploadFactory提交创建对象的请求,并用一个 uploadDatabase对象保存所有upload对象的外部状态,以便在程序运行过程中给upload共享对象设置外部状态

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;
        }
    }
})()

var id = 0;

window.startUpload = function( uploadType, files){
    for(var i=0, file; file = files[i++];) {
        var uploadObj = uploadManager.add(,++id, uploadType, file.fileName, file.fizeSize);
    }
}

startUpload('plugin',[
    {
        fileName: '1.txt',
        fileSize: 1000
    },
    {
        fileName: '2.html',
        fileSize: 2000
    }
])

startUpload('flash',[
    {
        fileName: '3.txt',
        fileSize: 1000
    },
    {
        fileName: '4.html',
        fileSize: 2000
    }
])

享元模式重构之前的代码里一共创建了4个upload对象,而通过享元模式重构之后,对象的数量减少为2, 即使同时上传2000个文件,需要创建的upload对象数量依然是2

6. 享元模式的适用性

享元模式是一种良好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,需要分别多维护一个factory对象和一个manager对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。

享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时,便可以使用享元模式

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

7. 内部状态和外部状态

实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态存储在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象。

考虑两种极端情况,即对象没有外部状态和没有内部状态的时候

7.1 没有内部状态的享元

改写创建享元对象的工厂

var UploadFactory = (function(){
     var uploadObj;
     
     return {
         create: function() {
             if (uploadObj) {
                 return uploadObj
             }
             
             return uploadObj = new Upload();
         }
     }
 })();

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

7.2 没有外部状态的享元

8. 对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new, 而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。

对象池的原理很好理解,比如我们组人手一本《JavaScript权威指南》,从节约的角度来讲,这并不是很划算,因为大部分时间这些书都被闲置在各自的书架上,所以一开始就只买一本,或者建立一个小型图书馆(对象池),需要看书的时候就从图书馆里借,看完了之后再把书还回图书馆。如果同时有三个人要看这本书,而现在图书馆只有两本,那我们再马上去购买一本放入图书馆

对象池技术的应用非常广泛,HTTP连接池和数据库连接池都是其代表应用。在Web前端开发中,对象池使用最多的场景大概就是跟DOM有关的操作。很多空间和时间都消耗在了DOM节点上,如何避免频繁地创建和删除DOM节点就成了一个有意义的话题

8.1 对象池的实现

var objectPoolFactory = function(createObjFn) {
    var objectPool = [];
    
    return {
        create: function(){
            var obj = objectPool.length === 0 ? 
                createObjFn.apply(this, arguments) : objectPool.shift();
           return obj;     
        },
        recover: function(obj) { 
           objectPool.push(obj); // 对象池回收
        }
    }
}

利用 objectPoolFactory 创建一个装载一些 iframe的对象

var iframeFactory = objectPoolFactory(function(){
    var iframe = document.createElement('iframe');
        document.body.appendChild(iframe);
        
    iframe.onload = function(){
        iframe.onload = null;
        iframeFactory.recover(iframe)
    }   
    
    return iframe
});

var iframe1 = iframeFactory.create();
iframe1.src = 'http:// xxx1.com'

var iframe2 = iframeFactory.create();
iframe2.src = 'http:// xxx2.com'

setTimeOut(function(){
    var iframe3 = iframeFactory.create();
    iframe3.src = 'http:// xxx3.com'
}, 3000)

对象池是另外一种性能优化方案,跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。

9.小结

享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题