在现实生活中,工厂的一大价值,就是提高了人类物质生产的效率,促进了经济的发展。而代码世界的工厂模式,也是为了提升程序员 coding 的效率,准时下班,享受生活。
工厂模式,就是工厂的模式~
在工厂模式中,我们通过输入不同的参数(或者直接不输入),得到我们最终想要的成品,它封装的是我们重复制造、输出成品的过程,简化的是我们得到成品的便利程度。生活中,吃饭也有类似的道理,最麻烦的是我们自己买菜、洗菜、做饭、刷碗,最简单的是我们去饭店,只需要告诉饭店我们想吃什么就可以了。
作为一种设计模式,它代表的是一种思想,它是被过往历史的众多程序员验证过的,踩过坑得出的结论。那既然它能跨越历史的横流,在不同语言中被认可,那就说明,具体语法上的实现只是一个载体,而我们看一个个代码例子的目的是:理解它,为己所用,去建造更高效率的工厂。
那么,带着我们的目的,开始打怪升级吧:
构造函数
构造函数就是工厂模式的一个语法载体。比如我们声明一个物料,可以直接这样写:
const windy007 = {
className: '风机', // 物料分类
name: 'windy007', // 物料名称
price: '199', // 物料价格
}
但如果我要声明 100 个物料呢,直接写 100 个className
、name
、price
,显然不够优雅,所以就有了构造函数。
function getMaterial(className, name, price) {
this.className = className
this.name = name
this.price = price
}
const fan = getMaterial('风机', 'windy007', 199)
通过构造函数,我们可以把不变的属性名称进行封装,把变动的东西对外拓展。由此,在构造对象时,便省去了多个属性名称的重复工作。这在我们写列表 columns 的时候会很常见。
下一个问题是:假如我们要给每个物料分类补充上它的功能说明呢?比如说,风机的功能说明是“给电脑降温”,cpu 的功能是“提供计算能力”。
给工厂升级
我们可以创建多个构造函数:
function getFan(name, price) {
this.name = name
this.price = price
this.className = '风机'
this.function = '给机器降温'
}
function getCpu(name, price) {
this.name = name
this.price = price
this.className = 'cpu'
this.functions = '提供数据计算能力'
}
我们也可以选择把functions
作为参数传入构造函数中:
function getMaterial(className, name, price, functions) {
this.className = className
this.name = name
this.price = price
this.functions = functions
}
但是这两种方式都有它的”坏味道“。
第一种,name
和 price
的构造过程在每个类中是重复的,而且每个构造函数都会拥有 4 个属性,我们需要在每个构造函数内都写一遍吗?
第二种,物料分类和功能其实是一一对应的关系,难道我们每次输入一个分类,又要重新写一遍它的功能?
所以,我们可以对他进一步优化,将”不变的东西封装起来“,让使用者更加便利。
function Material(classification, name, price) {
this.class = classification
this.name = name
this.price = price
let functions
switch (classification) {
case 'fan':
functions = '给机器降温'
break
case 'cpu':
functions = '提供数据计算能力'
default:
break
}
this.functions = functions
}
通过上面的封装,工厂函数的使用者就更满意了。
当然,上面的这个例子还比较简单,下面“模拟”一个看起来很长很复杂的工厂函数,“番茄炒蛋”。
function cook(name) {
if (name !== '番茄炒蛋') return '咱不做这个'
// 买食材,4颗番茄,4个鸡蛋
const tomatoes = Array.from({ length: 4 }, () => '番茄')
const eggs = Array.from({ length: 4 }, () => '鸡蛋')
// 把番茄切块, 每个切4小块
const tomatoCube = tomatoes.map((item) => {
return Array.from({ length: 4 }, () => '小' + item + '块')
})
// 把鸡蛋打碎
const eggLiquid = eggs.length * 50 + 'ml鸡蛋液'
// 煎鸡蛋
const friedEgg = Array.from(
{ length: parseInt(eggLiquid) / 5 },
() => '小煎蛋'
)
// 把番茄放下去炒熟
const friedTomato = tomatoCube.flat().map((item) => {
return '软烂的' + item
})
// 把煎好的鸡蛋放下去一同翻炒
const tomatoEgg = friedEgg.concat(friedTomato)
let i = 0
while (i < 300) {
const randomIndex = Math.floor(Math.random() * tomatoEgg.length)
const theOne = tomatoEgg.splice(randomIndex, 1)[0]
tomatoEgg.push(theOne)
i++
}
// 装盘
const plate = []
while (tomatoEgg.length > 5) {
plate.push(tomatoEgg.splice(0, 5))
}
plate.push(tomatoEgg)
return plate
}
const cuisine = cook('番茄炒蛋')
console.log(cuisine)
/*
运行上面的代码,你会得到类似这样的结果
[
[ '软烂的小番茄块', '软烂的小番茄块', '小煎蛋', '软烂的小番茄块', '小煎蛋' ],
[ '小煎蛋', '小煎蛋', '小煎蛋', '小煎蛋', '小煎蛋' ],
[ '小煎蛋', '小煎蛋', '小煎蛋', '软烂的小番茄块', '小煎蛋' ],
[ '软烂的小番茄块', '小煎蛋', '小煎蛋', '小煎蛋', '小煎蛋' ],
[ '小煎蛋', '小煎蛋', '软烂的小番茄块', '小煎蛋', '软烂的小番茄块' ],
[ '软烂的小番茄块', '小煎蛋', '小煎蛋', '小煎蛋', '软烂的小番茄块' ],
[ '小煎蛋', '小煎蛋', '小煎蛋', '小煎蛋', '小煎蛋' ],
[ '小煎蛋', '小煎蛋', '软烂的小番茄块', '小煎蛋', '软烂的小番茄块' ],
[ '小煎蛋', '小煎蛋', '软烂的小番茄块', '软烂的小番茄块', '小煎蛋' ],
[ '小煎蛋', '软烂的小番茄块', '小煎蛋', '小煎蛋', '小煎蛋' ],
[ '小煎蛋', '软烂的小番茄块', '小煎蛋', '软烂的小番茄块', '小煎蛋' ],
[ '小煎蛋' ]
]
*/
在这个函数中,我们封装了大量的步骤、极大地优化了应用层。可以说,我们在具象上的封装已经取得了一个小胜利,如果这个函数在一万个地方被引用,那么它就是被复用了一万次,算得上“复用性”强。
但这个函数显然“拓展性”不强,因为它只能处理”番茄炒蛋“,现实中,我们不太可能这家店只吃个番茄蛋,那家店只吃个牛腩煲吧,所以说,我们需要一些抽象的能力。
打开工厂的业务空间——抽象工厂
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。
抽象的对立面是具象。在代码中,变量是抽象的,具体的数据是具象的。
抽象工厂,就是对具体工厂一步步抽取变量的过程,把不变的东西封装进工厂内,把变动的东西抽象化。比例那个“番茄炒蛋”,我们对它进行抽象,就可以变成传 2 个参数:食材、烹饪手法,而切菜、装盘的步骤是具有一致性的。
function cook(food, method) {}
const tomatoEgg = cook(['番茄', '鸡蛋'], '炒')
再进一步抽象的化,我们可以把装盘的容器也传进去。
function cook(food, method, container) {}
const tomatoEgg = cook(['番茄', '鸡蛋'], '炒', '盘子')
const eggplantCasserole = cooke(['茄子'], '煲', '砂锅')
再进一步,我们把大份还是小份给传进去。
function cook(food, method, container, size) {}
const tomatoEgg = cook(['番茄', '鸡蛋'], '炒', '盘子', '小份')
const eggplantCasserole = cooke(['茄子'], '煲', '砂锅', '中份')
随着我们抽取的变量越来越多,你会发现这个函数逐渐地不那么好用了。就好像我们点餐,要告诉服务员,放几个番茄、几个鸡蛋、放多少盐、翻炒多少次一样,显得很麻烦。
所以,极度抽象后的工厂,往往不是直接服务于一线业务的,它主要是对具象工厂进行基础规划。
代码的世界,往往是金字塔,功能大多是一层套一层,一层继承一层。如果说“番茄炒蛋”属于纵向的封装,那么“做菜”其实是横向的封装,它封装出一个基类,我们可以基于它快速构造出族类,依然可以减少很多的重复工作。
结尾
工厂模式是快速构造出目标数据的一种模式,它通过封装不变的数据、不变的逻辑行为,从而减少我们的重复工作。当面对另一个很相似的工厂时,我们可以把它们进行对比,将变动的东西抽取出来,对外支持拓展,从而让它具有更广泛的作用。