享元模式
介绍
- 共享内容(减少内存开销)
- 相同的数据,共享使用
示例
👩🏼🤝🧑🏻服装厂模特拍广告
假设有一个服装工厂,目前有 50 种男士服装和 50 种女士服装。为了推销商品,工厂决定生产一些塑料模特来穿服装拍广告照片。正常情况需要 50 个男模特和 50 个女模特,然后每一位穿一件服装来拍照,如下👇🏻
class Modal { constructor(sex, clothes) { this.sex = sex this.clothes = clothes }
takePhoto() { console.log(`sex: ${this.sex}, clothes: ${this.clothes}`) }}
for(let i = 0; i <= 50; i +=1 ) { const maleModel = new Modal('male', 'clothes' + i) maleModel.takePhoto()}
for(let j = 0; j <= 50; j +=1 ) { const femaleModel = new Modal('female', 'clothes' + j) femaleModel.takePhoto()}这样的话,目前一共 50 种男衣服,50 种女衣服,一共产生 100 个对象。如果生产规模扩大,生产 10000 种衣服,那么程序岂不是鸭梨山大崩溃了。 其实我们并不需要这么多塑料模特,男模特和女模特各一个就可以了,每次穿不同衣服进行拍照。
class Model { constructor(sex) { this.sex = sex }
takePhoto() { console.log(`sex: ${this.sex}, clothes: ${this.clothes}`) }}
const maleModel = new Model('male')const femaleModel = new Model('female')
for(let i = 0; i <= 50; i += 1 ) { maleModel.clothes = `clothes${i}` maleModel.takePhoto()}
for(let j = 0; j <= 50; j += 1 ) { femaleModel.clothes = `clothes${j}` femaleModel.takePhoto()}📚图书馆入库书籍
图书馆同样的书(ISBN号相同)可能存有很多本,我们不希望每次入库一本书都创建一个新的 book 实例,我们只想为ISBN号相同的书创建一个 book 实例。
class Book { constructor(title, author, isbn) { this.title = title; this.author = author; this.isbn = isbn }}在入库书的时候,如果库中已经有相同的 ISBN 号的书,那么我们就不希望再创建一个完全一样的 book 实例了。
const isbnNumbers = new Set();
const createBook = (title, author, isbn) => { const book = isbnNumbers.has(isbn)
if (book) { return book }
isbnNumbers.add(isbn)
return new Book(title, author, isbn)}通过 createBook 方法我们可以为相同的书创建一个 book 实例,一个图书馆通常同一本书会有很多副本,然后让我们编写一个书籍入库方法,返回一个新的 book 实例或者返回 已存在的 book 实例
const bookList = []const addBook = (title, author, isbn, availibility, sales) => { const book = { ...createBook(title, author, isbn), sales, availibility, isbn }
bookList.push(book) reutrn book}测试一下,我们入库 5 本书,其中有 2 本各有 1 本相同。
addBook('Book1', 'author1', 'AB123', false, 100)addBook('Book1', 'author1', 'AB123', true, 80)addBook('Book2', 'author2', 'CD456', true, 100)addBook('Book2', 'author2', 'CD456', false, 200)addBook('Book3', 'author3', 'EF789', false, 100)最终代码如下
class Book { constructor(title, author, isbn) { this.title = title; this.author = author; this.isbn = isbn }}
const bookList = []
const addBook = (title, author, isbn, availibility, sales) => { const book = { ...createBook(title, author, isbn), sales, availibility, isbn }
bookList.push(book)
return book}
const isbnNumbers = new Set();
const createBook = (title, author, isbn) => { const book = isbnNumbers.has(isbn)
if (book) { return book }
isbnNumbers.add(isbn)
return new Book(title, author, isbn)}
addBook('Book1', 'author1', 'AB123', false, 100)addBook('Book1', 'author1', 'AB123', true, 80)addBook('Book2', 'author2', 'CD456', true, 100)addBook('Book2', 'author2', 'CD456', false, 200)addBook('Book3', 'author3', 'EF789', false, 100)
console.log(`Total amount of copies: ${bookList.length}`)console.log(`Total amount of books: ${isbnNumbers.size}`)
// Total amount of copies: 5// Total amount of books: 3💻文件上传
假设文件只有浏览器插件和表单上传两种方式,用户选择文件之后,插件和表单都会通知调用 Window 下的一个全局函数,叫做 startUpload, 用户选择的文件列表被组合成一个数组 files 塞入参数列表中。
let id = 0;window.startUpload = (uploadType, files) => { files.forEach(file => { const uploadObj = new Upload(uploadType, file.fileName, file.fileSize) uploadObj.init(id++) })}
class Upload { constructor(uploadType, fileName, fileSize) { this.uploadType = uploadType this.fileName = fileName this.fileSize = fileSize this.dom = null }
init(id) { this.id = id this.dom = document.createElement("div") this.dom.innerHTML = ` <span> 文件名称: ${this.fileName}, 文件大小:${this.fileSize} kb </span> <button class="del-file"> 删除 </button> ` this.dom.querySelector('.del-file').onclick = () => {this.delFile()} document.body.appendChild(this.dom) }
delFile() { // 文件小于 3000 kb 时,直接删除 if (this.fileSize < 3000) { return this.dom.parentNode.removeChild(this.dom) }
// 提示框提示删除 if (window.confirm('确定要删除该文件吗?'+ this.fileName)) { return this.dom.parentNode.removeChild(this.dom) } }}
// 创建 3 个插件上传对象和 3 个表单上传对象startUpload('plugin', [ { fileName: '1.txt', fileSize: 1000 }, { fileName: '2.txt', fileSize: 3000, }, { fileName: '3.txt', fileSize: 5000, }])
startUpload('form', [ { fileName: '4.txt', fileSize: 1000 }, { fileName: '5.txt', fileSize: 3000, }, { fileName: '6.txt', fileSize: 5000, }])上段代码中,需要上传多少文件,就创建了多少个 upload 对象。
首先需要判断一下是内部状态还是外部状态
- 内部状态储存于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
在这个上传的例子中,uploadType 是 Upload 所必需依赖的属性(不同类型的上传方式原理不同); fileName 和 fileSize 是不会被共享的,所以划分为外部状态
下面使用享元模式重构一下:
class Upload { constructor(uploadType) { this.uploadType = uploadType }
delFile(id) { uploadManager.setExternalState(id, this) // 文件小于 3000 kb 时,直接删除 if (this.fileSize < 3000) { return this.dom.parentNode.removeChild(this.dom) }
// 提示框提示删除 if (window.confirm('确定要删除该文件吗?'+ this.fileName)) { return this.dom.parentNode.removeChild(this.dom) } }}
class uploadManager { constructor() { this.uploadDatabase = {} }}设计原则验证
- 将相同的部分抽象出来
- 符合开放封闭原则