JavaScript设计模式-创建型-建造者模式
概要
建造者模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。
问题
假设我们要创建一个复杂的对象,构造这个对象时,有大量的基础变量和嵌套对象进行初始化。这种的初始化代码一般会隐藏在一个包含大量参数的庞大构造函数中。
上图,我们如何来创建 房屋 House
对象。
创建一个简单的房子,我们需要四面墙,屋顶,窗户和地板。但是如果我们想做一个功能更多的房子,比如说再加游泳池,我们会怎么做?
- 创建
House
基础类,然后再扩展创建一系列的子类来完成任务,最后我们面临着维护数量很大的子类集。
class House {
constructor() {
this.windows = 4
this.doors = 2
this.rooms = 4
}
}
class HouseWithSwimmingPool extends House {
constructor() {
super()
this.hasSwimPool = true
}
}
...
- 另一种方法是,不用扩展子类,可以在
House
基础类,通过接入参数(尽可能包含覆盖所有需求)的超级构造函数,通过参数的传递,来控制房子对象的构建。这种方法可以避免生成子类,但他的问题是很多参数不是每次都要用上,House
基础类接收参数的时候还要控制顺序,生成的对象很多参数没有用处。这些的问题使得对于构造函数的调用十分不简洁。
class House {
constructor(windows, doors, rooms, hasGarage, hasSwimPool, hasStatues, hasGarden) {
this.windows = windows
this.doors = doors
this.rooms = rooms
this.hasSwimPool = hasSwimPool
this.statues = hasStatues
this.garden = hasGarden
}
}
new House(4, 2, 4, null, true, null, null)
解决方案
建造者模式建议将对象构造代码从产品类中抽离出来,将其放在Builder
建造者类中。
该模式会将对象构造过程分为一组步骤,分布执行,比如buildWalls
创建墙壁和buildDoor
创建房门等步骤。每次创建对象时,都需要通过Builder
建造者类执行一些列步骤。这里Builder
建造者类重点在于不需要调用所有的步骤,可以安装产品类型的需求,调用你需要的步骤来生成你的最后构建对象。
当我们需要创建不同形式的产品时,其中某些构造步骤会需要不同的实现。比如,木屋的房门需要用木头来做,城堡的房门需要用石头来做。
这种情况下,我们可以创建多个不同的Builder
建造者类,用不同方式实现一组相同的创建步骤。然后在创建过程中使用这些不同的建造者类来生成不同的产品类型对象。
不同Builder
建造者类以不同方式执行相同的任务。
第一个建造者用木头和玻璃建造一切,第二个用石头和铁建造一切,第三个用黄金和钻石建造一切。通过调用相同的步骤,你可以从第一个建造者那里得到一个普通的房子,从第二个建造者那里得到一个小城堡,从第三个建造者那里得到一个宫殿。
Builder
建造者类是提供了对象构造过程的所有步骤方法。我们可以进一步的将一系列对建造者步骤的调用提取到一个类中,这个类被称为Director
主管类。Director主管类
只定义了执行构建步骤的顺序。
Director
主管类,知道需要哪些创建步骤才能获得可正常使用的产品。
严格来说,应用中可以不一定需要Director
主管类这个角色。客户端可以按照特定的顺序直接调用Builder
建造者类来完成产品构建的工作。但是Director
主管类非常适合放入各种例行构造流程, 以便在程序中反复使用。
此外, 对于客户端代码来说, Director
主管类完全隐藏了产品构造细节。 客户端只需要将一个建造者类与主管类关联, 然后使用主管类来构造产品, 就能从建造者处获得构造结果了。
建造者模式结构图
-
建造者 (Builder) 接口声明在所有类型建造者中通用的产品构造步骤。
-
具体建造者 (Concrete Builders) 提供构造过程的不同实现。 具体建造者也可以构造不遵循通用接口的产品。
-
产品 (Products) 是最终生成的对象。 由不同建造者构造的产品无需属于同一类层次结构或接口。
-
主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
-
客户端 (Client) 必须将某个建造者对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用建造者对象完成后续所有的构造任务。 但在客户端将建造者对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的建造者。
举例
在电商中有多种不同类型的商品 普通实物商品,电子卡券商品,虚拟视频学习商品 等多种不同的商品,他们都是商品但是他们的属性却不一样,电子卡券:独有券码,学习视频:独有视频链接等。
商品有很多类型,并且有很多的属性,不同类型的商品某些属性也各不相同,当产品对象属性复杂,需要进行大量情况不一的配置时,我们不妨来试试建造者模式。
/**
* 基础商品Item类
* name 商品名称
* type 商品类型 1普通商品,2卡券商品,3视频商品
* code type === 2 时才需要
* url type === 3 时才需要
*/
class Item {
setItemName(name) {
this.name = name
}
setItemType(type) {
this.type = type
}
setItemCode(code) {
this.code = code
}
setItemUrl(url) {
this.url = url
}
}
/**
* ItemBuilder 抽象建造者类
* Builder 接口声明在所有类型生成器中通用的产品构造步骤
*/
class ItemBuilder {
buildItemName() {
throw new Error('buildItemName 方法没有实现!')
}
buildItemType() {
throw new Error('buildItemType 方法没有实现!')
}
buildItemCode() {
throw new Error('buildItemCode 方法没有实现!')
}
buildItemUrl() {
throw new Error('buildItemUrl 方法没有实现!')
}
getResult() {
throw new Error('getResult 方法没有实现!')
}
}
/**
* 具体建造者类 遵循抽象建造者接口并提供具体实现步骤
* 程序中可能会有多个以不同方式实现的生成器变体。
*/
class ItemConcreteBuilder extends ItemBuilder {
constructor() {
super()
this.reset()
}
reset() {
this.item = new Item()
}
buildItemName(name) {
this.item.setItemName(name)
}
buildItemType(type) {
this.item.setItemType(type)
}
buildItemCode(code) {
this.item.setItemCode(code)
}
buildItemUrl(url) {
this.item.setItemUrl(url)
}
getResult() {
let product = this.item
this.reset()
return product
}
}
/**
* 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
* 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
* 不是必需的。
*/
class Director {
// 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通
// 过这种方式改变最新组装完毕的产品的最终类型。
setBuilder(builder) {
this.builder = builder
}
// 主管可使用同样的生成步骤创建多个产品变体。
constructBaseItem({ name } = {}) {
this.builder.reset()
this.builder.buildItemName(name)
}
constructNormalItem({ name } = {}) {
this.builder.reset()
this.builder.buildItemName(name)
this.builder.buildItemType('1')
}
constructCardItem({ name, code } = {}) {
this.builder.reset()
this.builder.buildItemName(name)
this.builder.buildItemCode(code)
this.builder.buildItemType('2')
}
}
function createItem() {
let director = new Director()
let builder = new ItemConcreteBuilder()
director.setBuilder(builder)
// 创建普通商品
director.constructNormalItem({ name: '普通商品' })
let normalItem = builder.getResult()
// 创建卡券商品
director.constructCardItem({ name: '卡券商品', code: '5435462' })
let cardItem = builder.getResult()
// 不使用 Director主管类 客户端直接调用 具体建造者 分步骤 创建对象
// 创建视频商品
director.constructBaseItem({ name: '视频商品' })
builder.buildItemType('3')
builder.buildItemUrl('http://blog.cheerspublishing.cn/')
let videoItem = builder.getResult()
console.log(normalItem)
console.log(cardItem)
console.log(videoItem)
}
createItem()
output
{
"name": "普通商品",
"type": "1"
}
{
"name": "卡券商品",
"code": "5435462",
"type": "2"
}
{
"name": "视频商品",
"type": "3",
"url": "http://blog.cheerspublishing.cn/"
}
总结
- 优点:
- 良好的封装性, 使用建造者模式可以使客户端不必知道产品内部组成的细节。
- 建造者独立,容易扩展。
- 将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰。
- 缺点:
- 会产生多余的Builder对象以及Director对象,消耗内存。