享元模式

286 阅读2分钟

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。

1. 初识享元模式

假设有个内衣工厂,目前的产品有 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(); 
};

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

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

2. 内部状态与外部状态

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。

  • 内部状态存储于对象内部。

  • 内部状态可以被一些对象共享。

  • 内部状态独立于具体的场景,通常不会改变。

  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式

3. 享元模式的通用结构

上面的例子并不是一个完整的享元模式,在这个例子中还存在以下两个问题

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

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

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

4.文件上传的例子

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

享元模式重构文件上传

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

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

//工厂进行对象实例化
var UploadFactory = (function() {
  var createdFlyWeightObjs = {};
  return {
    create: function(uploadType) {
      if (createdFlyWeightObjs[uploadType]) {
        return createdFlyWeightObjs[uploadType];
      }
      return (createdFlyWeightObjs[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) {
  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
  }
]);

5.享元模式的适用性

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个 factory 对象和一个 manager 对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。

  • 一个程序中使用了大量的相似对象。

  • 由于使用了大量对象,造成很大的内存开销。

  • 对象的大多数状态都可以变为外部状态。

  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。