工厂模式——效率提升的标志

724 阅读7分钟

在现实生活中,工厂的一大价值,就是提高了人类物质生产的效率,促进了经济的发展。而代码世界的工厂模式,也是为了提升程序员 coding 的效率,准时下班,享受生活。

工厂模式,就是工厂的模式~

在工厂模式中,我们通过输入不同的参数(或者直接不输入),得到我们最终想要的成品,它封装的是我们重复制造、输出成品的过程,简化的是我们得到成品的便利程度。生活中,吃饭也有类似的道理,最麻烦的是我们自己买菜、洗菜、做饭、刷碗,最简单的是我们去饭店,只需要告诉饭店我们想吃什么就可以了。

作为一种设计模式,它代表的是一种思想,它是被过往历史的众多程序员验证过的,踩过坑得出的结论。那既然它能跨越历史的横流,在不同语言中被认可,那就说明,具体语法上的实现只是一个载体,而我们看一个个代码例子的目的是:理解它,为己所用,去建造更高效率的工厂。

那么,带着我们的目的,开始打怪升级吧:

构造函数

构造函数就是工厂模式的一个语法载体。比如我们声明一个物料,可以直接这样写:

const windy007 = {
  className: '风机', // 物料分类
  name: 'windy007', // 物料名称
  price: '199', // 物料价格
}

但如果我要声明 100 个物料呢,直接写 100 个classNamenameprice,显然不够优雅,所以就有了构造函数。

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
}

但是这两种方式都有它的”坏味道“。

第一种,nameprice 的构造过程在每个类中是重复的,而且每个构造函数都会拥有 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(['茄子'], '煲', '砂锅', '中份')

随着我们抽取的变量越来越多,你会发现这个函数逐渐地不那么好用了。就好像我们点餐,要告诉服务员,放几个番茄、几个鸡蛋、放多少盐、翻炒多少次一样,显得很麻烦。

所以,极度抽象后的工厂,往往不是直接服务于一线业务的,它主要是对具象工厂进行基础规划。

代码的世界,往往是金字塔,功能大多是一层套一层,一层继承一层。如果说“番茄炒蛋”属于纵向的封装,那么“做菜”其实是横向的封装,它封装出一个基类,我们可以基于它快速构造出族类,依然可以减少很多的重复工作。

继承.png

结尾

工厂模式是快速构造出目标数据的一种模式,它通过封装不变的数据、不变的逻辑行为,从而减少我们的重复工作。当面对另一个很相似的工厂时,我们可以把它们进行对比,将变动的东西抽取出来,对外支持拓展,从而让它具有更广泛的作用。