享元模式

Mr.Hotsuitor大约 3 分钟设计模式享元模式

享元模式

驾考现场的考试车 众多考生,不是每人一辆考试车,而是共享n辆考试车,轮流来

var candidateNum = 10 // 考生数量
var examCarNum = 0 // 驾考车数量

/**
 * @description: 驾考车构造函数
 */
function ExamCar(carType) {
  examCarNum++
  this.carId = examCarNum
  this.carType = carType ? '手动挡' : '自动挡'
}

ExamCar.prototype.examine = function(candidateId) {
  console.log('考生-' + candidateId + ' 在' + this.carType + '驾考车-' + this.carId + '上考试')
}

var manualExamCar = new ExamCar(true)
var autoExamCar = new ExamCar(false)

for (var candidateId = 1; candidateId <= candidateNum; candidateId++) {
  var examCar = candidateId % 2 ? manualExamCar : autoExamCar
  examCar.examine(candidateId)
}

console.log('驾考车总数-' + examCarNum)

可以看到我们使用 2 个驾考车实例就实现了刚刚 10 个驾考车实例实现的功能。这是仅有 10 个考生的情况,如果有几百上千考生,这时我们节约的内存就比较可观了,这就是享元模式要达到的目的。

享元模式改进

let examCarNum = 0 // 驾考车总数

/* 驾考车对象 */
class ExamCar {
  constructor(carType) {
    examCarNum++
    this.carId = examCarNum
    this.carType = carType ? '手动档' : '自动档'
    this.usingState = false // 是否正在使用
  }

  /* 在本车上考试 */
  examine(candidateId) {
    return new Promise((resolve) => {
      this.usingState = true
      console.log(`考生- ${candidateId} 开始在${this.carType}驾考车- ${this.carId} 上考试`)
      setTimeout(() => {
        this.usingState = false
        console.log(`%c考生- ${candidateId}${this.carType}驾考车- ${this.carId} 上考试完毕`, 'color:#f40')
        resolve() // 0~2秒后考试完毕
      }, Math.random() * 2000)
    })
  }
}

/* 手动档汽车对象池 */
ManualExamCarPool = {
  _pool: [], // 驾考车对象池
  _candidateQueue: [], // 考生队列

  /* 注册考生 ID 列表 */
  registCandidates(candidateList) {
    candidateList.forEach((candidateId) => this.registCandidate(candidateId))
  },

  /* 注册手动档考生 */
  registCandidate(candidateId) {
    const examCar = this.getManualExamCar() // 找一个未被占用的手动档驾考车
    if (examCar) {
      examCar
        .examine(candidateId) // 开始考试,考完了让队列中的下一个考生开始考试
        .then(() => {
          const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()
          nextCandidateId && this.registCandidate(nextCandidateId)
        })
    } else this._candidateQueue.push(candidateId)
  },

  /* 注册手动档车 */
  initManualExamCar(manualExamCarNum) {
    for (let i = 1; i <= manualExamCarNum; i++) {
      this._pool.push(new ExamCar(true))
    }
  },

  /* 获取状态为未被占用的手动档车 */
  getManualExamCar() {
    return this._pool.find((car) => !car.usingState)
  },
}

ManualExamCarPool.initManualExamCar(3) // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // 10个考生来考试

资源池

上面这种改进的模式一般叫做资源池(Resource Pool),或者叫对象池(Object Pool),可以当作是享元模式的升级版,实现不一样,但是目的相同。资源池一般维护一个装载对象的池子,封装有获取、释放资源的方法,当需要对象的时候直接从资源池中获取,使用完毕之后释放资源等待下次被获取。

在上面的例子中,驾考车相当于有限资源,考生作为访问者根据资源的使用情况从资源池中获取资源,如果资源池中的资源都正在被占用,要么资源池创建新的资源,要么访问者等待占用的资源被释放。

资源池在后端应用相当广泛,比如缓冲池、连接池、线程池、字符常量池等场景,前端使用场景不多,但是也有使用,比如有些频繁的 DOM 创建销毁操作,就可以引入对象池来节约一些 DOM 创建损耗。

下面介绍资源池的几种主要应用。

  • Event Loop
  • 缓存
  • 连接池,一般数据库连接操作
    • 在 Node.js 中使用 mysql 模块的连接池创建连接:
      
      var mysql = require('mysql')
      
      var pool = mysql.createPool({     // 创建数据库连接池
          host: 'localhost',
          user: 'root',         // 用户名
          password: '123456',   // 密码
          database: 'db',       // 制定数据库
          port: '3306'          // 端口号
      })
      
      // 从连接池中获取一个连接,进行增删改查
      pool.getConnection(function(err, connection) {
          // ... 数据库操作
          connection.release()  // 将连接释放回连接池中
      })
      
      // 关闭连接池
      pool.end()
      
  • 字符常量池

优缺点

优点:

  • 由于减少了系统中的对象数量,提高了程序运行效率和性能,精简了内存占用,加快运行速度;
  • 外部状态相对独立,不会影响到内部状态,所以享元对象能够在不同的环境被共享;

缺点:

  • 引入了共享对象,使对象结构变得复杂;
  • 共享对象的创建、销毁等需要维护,带来额外的复杂度(如果需要把共享对象维护起来的话);

适用场景

  • 如果一个程序中大量使用了相同或相似对象,那么可以考虑引入享元模式;
  • 如果使用了大量相同或相似对象,并造成了比较大的内存开销;
  • 对象的大多数状态可以被转变为外部状态;
  • 剥离出对象的外部状态后,可以使用相对较少的共享对象取代大量对象;
  • 在一些程序中,如果引入享元模式对系统的性能和内存的占用影响不大时,比如目标对象不多,或者场景比较简单,则不需要引入,以免适得其反。

1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。