书中将 Singleton 单例模式、Prototype 原型模式、Builder 建造者模式、Abstract Factory 抽象工厂模式归入生成实例的相关的设计模式。
- 单例模式:只允许生成一个实例的模式,核心在于限制为单一实例。
- 原型模式:通过复制生成实例,核心在于复制现有实例,避免重走创建过程。
- 建造者模式:通过组装生成复杂实例,并将组装构造过程抽离至独立的指挥者类,核心在于侧重组装。
- 抽象工厂模式:提供一个创建一组相关或相互依赖对象的接口,核心在于同一个工厂可生成不同的相关对象。
Singleton 单例模式
定义
确保任何情况下都绝对只有一个实例的模式叫做单例模式。
角色
由于如 Java 中,可以实现构造函数的私有化(禁止外部调用),所以单例模式下只有一个角色。
Singleton:有一个返回唯一实例的方法。
JavaScript 实现
由于 JavaScript 中不能很好的实现构造函数的私有化,所以需要有其他的类进行配合才能实现,且不能完全参照书中所给 Java 实现。
单例模式的实现又可以分为饿汉式(提前创建,需要时返回)和懒汉式(需要时创建)。
- 饿汉式:
const instance = Symbol('instance') class Constructor { constructor () { this.time = Date.now() } } const singleton = { [instance]: new Constructor(), // 提前生成 getInstance () { return this[instance] } }
- 懒汉式:
const instance = Symbol('instance') class Constructor { constructor () { this.time = Date.now() } } const singleton = { [instance]: null, getInstance () { // 获取时才检查并在没有时生成 if (!this[instance]) { this[instance] = new Constructor() } return this[instance] } }
当然还有使用构造函数返回、闭包等实现方式。
思考
在明确只提供给用户一个实例的情况下,如组件复用、避免多个实例可能存在配置干扰时可以使用单例模式。
Prototype 原型模式
定义
不通过构造函数(类)生成实例,根据实例来生成实例(即克隆)的模式叫做 Prototype 模式。
注意:这里的 Prototype 并不是 JavaScript 中的原型链的原型,而是跟贴近模型的意思
角色
书中给出的是带有管理者的原型模式。
- 原型:接口或抽象类,定义具体的原型必须要实现的方法(克隆方法)
- 具体的原型:实现具体的克隆算法。
- 管理者:存储一系列的原型,并对外提供生成具体实例的接口。
JavaScript 实现
在 Java 的实现中,在所有 Java 类的基类上有 clone 方法的实现,方法内对当前调用者是否实现了标记接口 interface Cloneable
做了判断,未实现时会抛出异常并阻止克隆。
在 JavaScript 中还未提供统一的 clone 方法,且实现 Cloneable 没有必要。所以我们并不完全参照 Java 实现。
准备部分:
// 克隆算法需要自己实现
// 要注意出实例自身字段的克隆外还要处理好新的实例的原型链指向
// 为方便测试,我们使用 mock 的函数
function clone () {
console.log('copy')
return this
}
// 原型
class ObjectCloneable {
// 不直接把 clone 方法添加到 Object 的 prototype 属性上
clone () {
return clone.call(this)
}
createClone () {
throw new Error('需实现 ObjectCloneable 类的 createClone 方法')
}
}
// 具体的原型类,圆
class Circle extends ObjectCloneable {
constructor () {
super()
this.name = '圆'
}
createClone () {
return this.clone()
}
}
// 具体的原型类,正方形
class Square extends ObjectCloneable {
constructor () {
super()
this.name = '正方形'
}
createClone () {
return this.clone()
}
}
管理者:
class Manager {
constructor () {
this.pool = {}
this.register('circle', new Circle())
this.register('square', new Square())
}
register (name, proto) {
this.pool[name] = proto
}
create (name) {
const proto = this.pool[name]
if (proto) {
return proto.createClone()
}
}
}
// 使用
const manager = new Manager()
// 获取新的实例
manager.create('circle')
manager.create('square')
思考
原型模式并不常见,往往是在通过 new 创建实例效率太低(权限、新的数据请求)时才会使用。
带有管理者的模式一般出现在包含成套原型的系统中,如提供成套不同类型表单的克隆功能的系统,将这套表单原型实例通过表单类型名称的方式注册到 manager 中,供使用者通过表单类型名称获取新的克隆实例。
当数量或复杂度不够时可直接调用 clone 方法获取新的实例。
const form = new form()
const myForm = form.clone()
拓展
-
JavaScript 中的原型模式
JavaScript 中也有叫做原型模式的概念,也可以实现实例的“克隆”,但与设计模式中的原型模式不同,为依靠原型链的概念,将原型对象添加到新对象的原型链上,从而实现新对象的属性值、方法与原型对象相同。
const proto = {a: 1} const newInstance = Object.create(proto)// 或其他方式 newInstance.a // 输出为 1 // 但属性 a 存在于 newInstance.__proto__,后者指向 proto 对象
但是,在 JavaScript 中,使用原型模式这一设计模式实现实例生成时也不能忽略了对原型链的处理。
-
Java 中标记接口 Cloneable 的设计问题
Builder 建造者模式
定义
指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。
角色
- 产品类:即产品,定义了产品的模型,个人感觉是非必要的,在 JavaScript 中可通过其他方式定义模型。
- 抽象建造者类:定义了建造者具体的接口
- 具体建造者类:实现抽象建造者类定义的 api ,并通过这一系列 api 生成最终的产品(实例)
- 指挥者:在其内部调用具体的建造者的 api,致使生成实例
JavaScript 实现
// 产品类
class Page {
constructor () {
this.header = null
this.main = null
this.footer = null
}
}
// 抽象建造者类
class Builder {
constructor () {
this.page = new Page()
}
buildHeader () {
throw new Error('需实现 Builder 类的 buildHeader 方法')
}
buildMain () {
throw new Error('需实现 Builder 类的 buildMain 方法')
}
buildFooter () {
throw new Error('需实现 Builder 类的 buildFooter 方法')
}
getDom () {
const fragment = document.createDocumentFragment()
fragment.append(this.page.header)
fragment.append(this.page.main)
fragment.append(this.page.footer)
return fragment
}
}
// 具体建造者类 1
class HomePageBuilder extends Builder {
constructor () {
super()
}
buildHeader () {
const header = document.createElement('header')
header.innerHTML = '首页'
this.page.header = header
}
buildMain () {
const main = document.createElement('div')
main.innerHTML = '职位推荐'
this.page.main = main
}
buildFooter () {
const footer = document.createElement('footer')
footer.innerHTML = '版权'
this.page.footer = footer
}
}
// 具体建造者类 2
class IPageBuilder extends Builder {
constructor () {
super()
}
buildHeader () {
const header = document.createElement('h1')
header.innerHTML = '欢迎'
this.page.header = header
}
buildMain () {
const main = document.createElement('ul')
main.innerHTML = '浏览历史<li>1</li><li>2</li>'
this.page.main = main
}
buildFooter () {
const footer = document.createElement('footer')
footer.innerHTML = '联系我们'
this.page.footer = footer
}
}
// 具体建造者类 等等
class XPageBuilder extends Builder {
//
}
// 指挥者类 Director
class Director {
constructor (builder) {
this.builder = builder
}
build () {
this.builder.buildHeader()
this.builder.buildMain()
this.builder.buildFooter()
return this.builder.getDom()
}
}
// 使用者
const homePageBulder = new HomePageBuilder()
// const iPageBuilder = new IPageBuilder()
const director = new Director(homePageBulder)
const homePage = director.build()
document.body.appendChild(homePage)
思考
虽然建造者定义了自己的建造步骤,但是这些建造步骤是指挥者角色中被(固化)调用串联的,所以,指挥者类依赖了建造者类,对建造者类熟知。
- 模式对比:
- 模板方法模式:模板方法模式同样固化了流程,但是流程是由父类控制,而在建造者模式中,则是由指挥者这一额外的类控制,同时指挥者类也是可替换的,及不同的指挥者类可进行不同顺序的创建过程。
- 工厂方法模式:工厂方法模式中由于工厂方法本身可以使用模板方法模式实现,那么它也是可以封装一定构造流程的。个人认为工厂方法模式是先生成实例再做线性的流程处理,而建造者模式则是由建造者实现每个部件(的实例化),再通过抽离的构造步骤拼接出最终的产品。其实不必过于纠结两者的区别,在实际开发中可以都做尝试。
- 运用:如果仅仅只有一种建造者类,其实可以将构造步骤直接内置于建造者类中。不必额外添加指挥者,增加代码和使用复杂度。
class Builder { constructor () { this.page = new Page() } buildHeader () { const header = document.createElement('header') header.innerHTML = '首页' this.page.header = header } buildMain () { const main = document.createElement('div') main.innerHTML = '职位推荐' this.page.main = main } buildFooter () { const footer = document.createElement('footer') footer.innerHTML = '版权' this.page.footer = footer } getDom () { const fragment = document.createDocumentFragment() fragment.append(this.page.header) fragment.append(this.page.main) fragment.append(this.page.footer) return fragment } }
Abstract Factory 抽象工厂模式
定义
提供创建一组相关或相互依赖对象的接口,而且无需指定他们的具体类。
角色
- 抽象产品类:有多个,定义了不同产品的 API。
- 实现产品类:同样有多个。
- 抽象工厂类:定义用户生成抽象产品的接口,即需要提供哪些产品。
- 实现工厂类:负责实现抽象工厂类定义的生产不同产品的接口。
JavaScript 实现
由于涉及类比较多,先以中文描述需要做的事情。 现规定所有的科技工厂都能生产手机和手环这两种产品,我们需要实现小米和华为这两个具体的工厂。
// 抽象工厂类
class TechnologyCompany {
createPhone () {
throw new Error('需实现 TechnologyCompany 类的 createPhone 方法')
}
createBand () {
throw new Error('需实现 TechnologyCompany 类的 createBand 方法')
}
}
// 抽象手机产品类
class Phone {
call () {
throw new Error('需实现 Phone 类的 call 方法')
}
play () {
throw new Error('需实现 Phone 类的 play 方法')
}
}
// 抽象手环产品类
class Band {
heart () {
throw new Error('需实现 Band 类的 heart 方法')
}
walk () {
throw new Error('需实现 Band 类的 walk 方法')
}
}
// 实现小米部分
// 实现产品类
class MiPhone {
call () {
console.log('语音呼叫')
}
play () {
console.log('游戏模式')
}
}
class MiBand {
heart () {
console.log('Are you OK?')
}
walk () {
console.log('Are you OK?')
}
}
class Xiaomi extends TechnologyCompany {
createPhone () {
return new MiPhone()
}
createBand () {
return new MiBand()
}
}
// 实现华为部分
// 实现产品类
class MatePhone {
call () {
console.log('智能呼叫')
}
play () {
console.log('省电模式')
}
}
class B3Band {
heart () {
console.log('最大心率')
}
walk () {
console.log('今日运动量')
}
}
class Huawei extends TechnologyCompany {
createPhone () {
return new MatePhone()
}
createBand () {
return new B3Band()
}
}
// 使用者
const huawei = new Huawei()
const metaPhone = huawei.createPhone()
const b3Band = huawei.createBand()
metaPhone.play()
b3Band.walk()
const xiaomi = new Xiaomi()
const miPhone = xiaomi.createPhone()
const miBand = xiaomi.createBand()
miPhone.play()
miBand.walk()
思考
-
开闭原则:抽象工厂模式是将产品归类,将这些产品的生成归纳到一个工厂的不同接口上,交由不同的实现工厂去提供实现。那么需要提供不同实现时,只需要提供新的实现产品类和实现工厂类,不需要修改原有实现和使用方的代码,但是一旦需要新的产品,那么就需要修改基础的抽象工厂类,添加新的接口,从而导致所有已存在的实现工厂类都要添加此接口的实现。
-
模式对比:
- 工厂方法模式:对比来讲,每个生成产品的方法都是一个工厂方法,不同的是,在工厂方法模式中仅仅将实例化交给了实现子类,而在抽象工厂方法模式中,实现子类会实现产品生产的整个过程。
- 建造者模式:建造者模式中,具体的建造者也有创建一系列“产品”的接口,但不同的是,在建造者模式中这些产品都是最终产品的一部分,最终会在指挥者中拼接成最终的产品。而抽象工厂模式中,各个接口创建出的产品更倾向于完整可使用的个体。