JavaScript设计模式之享元模式

3,509 阅读2分钟

本周我们来学习一种为了优化系统性能而生的设计模式——享元模式。就是分享之意,指一物被众人共享,而这也正是该模式的终旨所在,意为单元,蝇量级的个体,该模式的核心就是使用共享技术来有效的支持大量的细粒度对象。

定义

享元模式要求将对象的属性划分为内部状态和外部状态(状态在这里通常指属性),其目标是尽量减少共享对象的数量,其UML图如下所示:

  • Flyweight 是抽象享元角色,为具体享元角色规定了必须实现的方法。
  • ConcreteFlyweight 是具体享元角色,实现抽象享元角色定义的方法。
  • FlyweightFactory 是享元工厂,负责创建和管理享元角色,它用于构造一个池容器,同时提供从池中获得对象的方法。
  • Client 是客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。

使用场景

相信说了这么多概念性的问题,大家都还没有明白到底什么是享元模式,或者是享元模式到底有什么用处?下面通过一个简单的例子来向大家说明。

有个服装厂,生产了男女服装各50种款式,为了推销需要找模特来拍照,正常可能会找男女模特各50个,每个模特分别穿一种服装拍一组照片。其代码实现如下:

// 模特类
class Modal {
    constructor(name, gender, clothes) {
        this.name = name
        this.gender = gender
        this.clothes = clothes
    }

    takePhoto() {
        console.log(`${this.gender}模特${this.name}穿${this.clothes}拍了张照`)
    }
}
// 穿衣拍照实现
for (let i = 0; i < 50; i++) {
    let manModel = new Modal(`张${i}`, '男', `服装${i}`)
    manModel.takePhoto()
}

for (let i = 50; i < 100; i++) {
    let womanModel = new Modal(`李${i}`, '女', `服装${i}`)
    womanModel.takePhoto()
}

运行结果如下图:

虽然实现了这个需求,想必也一定创建了100个模特对象。想象一下如果衣服种类无休止的增长下去,那么模特对象的数量也会增长下去,当对象多到一定数量程序必然会崩溃。

此时享元模式可以帮助我们来解决这个问题,仔细分析一下,其实不管有多少种类衣服,我们只需要男女各一个模特来穿衣服进行拍照也可实现该需求,实现享元模式的核心就是学会划分内部状态和外部状态,下面几条经验可以供我们快速对内部外部状态进行划分:

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

通过上面的方法进行分析后可以得出对于模特对象来说性别是其内部状态,而衣服是外部状态,下面我们使用享元模式来对这个例子进行改造

构建享元对象

class Modal {
    constructor(id, gender) {
        this.gender = gender
        this.name = `张${gender}${id}`
    }
}

构建享元工厂

class ModalFactory {
    //单例模式
    static create(id, gender) {
        if (this[gender]) {
            return this[gender]
        }
        return this[gender] = new Modal(id, gender)
    }
}

管理外部状态

class TakeClothesManager {
    // 添加衣服款式
    static addClothes(id, gender, clothes) {
        const modal = ModalFactory.create(id, gender)
        this[id] = {
            clothes,
            modal
        }
    }
    // 拍照
    static takePhoto(id) {
        const obj = this[id]
        console.log(`${obj.modal.gender}模特${obj.modal.name}穿${obj.clothes}拍了张照`)
    }
}

执行

for (let i = 0; i < 50; i++) {
    TakeClothesManager.addClothes(i, '男', `服装${i}`)
    TakeClothesManager.takePhoto(i)
}

for (let i = 50; i < 100; i++) {
    const {addClothes, takePhoto} = TakeClothesManager
    TakeClothesManager.addClothes(i, '女', `服装${i}`)
    TakeClothesManager.takePhoto(i)
}

运行结果图如下:

从运行结果可以看出只需要创建两个不同性别的模特对象便完成了同样的功能,这就是享元模式的优点所在。

总结

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

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

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