JavaScript 中工厂设计模式的威力

2025-05-25

JavaScript 中工厂设计模式的威力

帖子缩略图

作为程序员,我们在编写代码时总是努力做出正确的决策。这并非易事,尤其是当我们的代码随着时间的推移变得越来越大时。幸运的是,当合适的时机到来时,有一些行之有效的方法可以帮助我们选择一种实现方式而不是另一种。

如果你是编程新手,可能还没有遇到过需要用工厂模式来抽象复杂对象的情况。如果你打算以后继续写代码,那么这篇文章会对你有所帮助。

在本文中,我们将探讨 JavaScript 中工厂设计模式的强大之处,它能够将复杂对象分解为更简单的对象,从而避免不必要的复杂性。请记住,我们将遵循 DRY 原则作为最佳实践。

当我们想到现实世界中的工厂时,我们想到的是一个生产产品的实验室。当我们将其转化为代码时,工厂模式就是这样的。

假设我们正在构建一款 MMORPG 游戏,我们将介绍利用这种模式的部分,并了解它如何使我们的应用程序受益。

我们将有一个Game类,一个Profile用于在用户打开我们的软件时创建配置文件,以及配置文件将创建的四个类作为角色供我们的用户选择:

class Mag extends Character {}
class Thief extends Character {}
class Archer extends Character {}
class Warrior extends Character {}

class Profile {
  constructor(name, email = '') {
    this.name = name
    this.email = email
  }

  createCharacter(classType) {
    switch (classType) {
      case 'archer':
        this.character = new Archer()
        return this.character
      case 'mage':
        this.character = new Mage()
        return this.character
      case 'thief':
        this.character = new Thief()
        return this.character
      case 'warrior':
        this.character = new Warrior()
        return this.character
      default:
        throw new Error(
          `Invalid class type "${classType}". Choose one of: "archer", "mage", "thief", or "warrior"`,
        )
    }
  }

  synchronizeProfileContacts(anotherProfile) {
    // Do something to inherit anotherProfile's contacts
  }

  setName(name) {
    this.name = name
  }

  setEmail(email) {
    this.email = email
  }
}

class Game {
  constructor() {
    this.users = {}
  }

  createUser(name) {
    const user = new Profile(name)
    this.users[user.id] = user
    return user
  }
}

const game = new Game()
const bobsProfile = game.createUser('bob')
const bobsMage = bobsProfile.create('mage')
Enter fullscreen mode Exit fullscreen mode

三个月后,我们决定实现另一个名为 的角色类Shaman

为了做到这一点,我们必须创建类:

class Shaman extends Character {}
Enter fullscreen mode Exit fullscreen mode

当我们想让用户Shaman在更新后选择类并调用时profile.createCharacter我们会收到此错误:

Error: Invalid class type "shaman". Choose one of: "archer", "mage", "thief", or "warrior"
Enter fullscreen mode Exit fullscreen mode

这是因为我们必须改变类create的方法Profile

我们将其改为这样之后,它就可以工作了:

class Profile {
  constructor(name, email = '') {
    this.name = name
    this.email = email
  }

  createCharacter(classType) {
    switch (classType) {
      case 'archer':
        this.character = new Archer()
        return this.character
      case 'mage':
        this.character = new Mage()
        return this.character
      case 'shaman':
        this.character = new Shaman()
        return this.character
      case 'thief':
        this.character = new Thief()
        return this.character
      case 'warrior':
        this.character = new Warrior()
        return this.character
      default:
        throw new Error(
          `Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
        )
    }
  }

  synchronizeProfileContacts(anotherProfile) {
    // Do something to inherit anotherProfile's contacts
  }

  setName(name) {
    this.name = name
  }

  setEmail(email) {
    this.email = email
  }
}
Enter fullscreen mode Exit fullscreen mode

这就是工厂设计模式解决的问题。

如果我们想再添加 3 个角色类别怎么办?我们必须修改实现 1-3 次。

还记得我们提到过要遵循 DRY 原则吗?每个开发人员都应该遵循。这违反了这条规则!

如果你是编程新手,仅从我们目前的代码来看,这似乎不是什么大问题。因为我们的Game类只有一个createUser方法,但在现实世界中,MMORPG 游戏的代码量肯定会更大,因为它们需要添加各种必要的功能来提升游戏的娱乐价值。

我们的类可能有大量不同的方法实现Game许多功能,例如createTerrain,,,,,,,,createEquipmentcreateMonstercreateAttackcreatePotioncreateRaidcreateBuildingcreateShop

不幸的是,所有这些方法很可能都需要进一步扩展,因为它们各自都需要创建不同的类型。例如,createEquipment可能需要实现一种创建剑类装备、法杖、靴子、盔甲的方法,而这些装备很可能都需要生成更多类型的变体,例如剑和靴子的类型。

因此,如果我们想立即实现所有这些,我们必须像第一次编写类时一样更改每个方法Shaman,并且我们已经遭受了第一个错误,因为我们忘记在方法的实现中添加 ShamanProfile.createUser

如果我们停止这里的工厂建设,那么三个月后,情况很快就会变得难以承受,因为我们被迫尝试各种方法并进行改变。

当代码变大时,工厂模式就会发挥作用。

Profile.createCharacter如果可以保持不变,这样我们就不用再修改它了,会怎么样?它不需要知道它创建的是哪种类型的字符。只需要给它一个字符类,并将其存储在它的实例中。

如果我们想添加 10 个字符类,我们必须手动寻找相同的函数并更新它,即使它Profile不关心生成什么类型​​的字符类,因为它只关心setName和之类的方法synchronizeProfileContacts

我们可以抽象出该部分并将其放入工厂中以生产这些对象

class CharacterClassCreator {
  create(classType) {
    switch (classType) {
      case 'archer':
        return new Archer()
      case 'mage':
        return new Mage()
      case 'shaman':
        return new Shaman()
      case 'thief':
        return new Thief()
      case 'warrior':
        return new Warrior()
      default:
        throw new Error(
          `Invalid class type "${classType}". Choose one of: "archer", "mage", "thief", or "warrior"`,
        )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我们的Profile课程可以看起来更优雅,以适应这一变化:

class Profile {
  constructor(name, email = '') {
    this.name = name
    this.email = email
  }

  synchronizeProfileContacts(anotherProfile) {
    // Do something to inherit anotherProfile's contacts
  }

  setName(name) {
    this.name = name
  }

  setEmail(email) {
    this.email = email
  }

  setCharacter(character) {
    this.character = character
  }
}
Enter fullscreen mode Exit fullscreen mode

我们不再违反 DRY 原则了。太棒了!CharacterClassCreator如果我们想实现更多需要创建的角色类,只需要修改一下。我们赋予它的唯一职责就是生成不同的角色类对象。

以下是工厂建成之前的视觉效果:

不使用工厂设计模式的配置文件创建

现在看起来是这样的Profile

利用工厂设计模式的强大功能创建配置文件

太棒了!我们让配置文件看起来干净整洁。我们让Profile类只关注其逻辑。

如果你想知道CharacterClassCreator这件事的立场,那么这实际上是在幕后发生的事情:

使用角色类别创建器工厂创建个人资料

我们添加了一个中间人(工厂)来处理创建角色类的逻辑。从现在开始,每当我们需要更新该代码的实现时,我们只需要更改CharacterCreationClass

我希望你现在就能感受到它的好处。还记得我们之前讨论过我们的Game类最终会拥有的其他方法,比如createBuilding和 吗createTerrain?如果我们对所有这些方法都应用类似的工厂方法,那么流程将会是相同的。这使得每个类都能专注于自己的逻辑。

让我们继续我们的代码。

在MMORPG游戏中,不同角色职业穿戴不同的装备。

例如,魔法师通常使用法杖,战士穿着厚重的钢甲并携带剑,盗贼携带一两把匕首,弓箭手使用弩。

此外,如果用户注册帐户并购买某种类型的会员资格,通常会有一些额外福利。

它可能看起来像这样:

class Equipment {
  constructor(name) {
    this.name = name
  }
}

class CharacterClassCreator {
  async applyMembershipCode(code) {
    // return fetch(`https://www.mymmorpg.com/api/v1/membership-code?code=${code}`)
    //   .then((resp) => resp.json())
    //   .then((data) => data)
    return { equipments: [{ type: 'ore' }] }
  }

  async create(profile, classType) {
    const creatorMap = {
      archer: {
        Class: Archer,
      },
      mage: {
        Class: Mage,
      },
      shaman: {
        Class: Shaman,
      },
      thief: {
        Class: Thief,
      },
      warrior: {
        Class: Warrior,
      },
    }

    let character
    // Each character class has a different starter weapon
    let starterWeapon

    if (creatorMap[classType]) {
      const { Class, membership } = creatorMap[classType]
      character = new Class()

      if (character instanceof Archer) {
        starterWeapon = new Equipment('crossbow')
      } else if (character instanceof Mage) {
        starterWeapon = new Equipment('staff')
      } else if (character instanceof Shaman) {
        starterWeapon = new Equipment('claw')
      } else if (character instanceof Thief) {
        starterWeapon = [new Equipment('dagger'), new Equipment('dagger')]
      } else if (character instanceof Warrior) {
        starterWeapon = new Equipment('sword')
      }

      character.useEquipment(starterWeapon)

      if (typeof profile.code === 'number') {
        if (profile.code) {
          const { equipments: _equipments_ } = await this.applyMembershipCode(
            profile.code,
          )
          // There are equipments provided in addition to the starter weapon.
          // This is most likely the result of the user paying for extra stuff
          _equipments_.forEach((equipment) => {
            // For thief class that uses duo daggers
            if (Array.isArray(equipment)) {
              character.useEquipment(equipment[0])
              character.useEquipment(equipment[1])
            } else {
              character.useEquipment(equipment)
            }

            if (membership) {
              if (membership.status === 'gold') {
                // They bought a gold membership. Ensure we apply any starter equipment enhancents they bought with their membership at checkout when they created a new account
                if (membership.accessories) {
                  membership.accessories.forEach(({ accessory }) => {
                    if (accessory.type === 'ore') {
                      // Each ore has an 80% chance of increasing the strength of some weapon when using some enhancer
                      const { succeeded, equipment } = this.applyEnhancement(
                        starterWeapon,
                        accessory,
                      )
                      if (succeeded) starterWeapon = equipment
                    } else if (accessory.type === 'fun-wear') {
                      // They also bought something fancy just to feel really cool to their online friends
                      character.useEquipment(new Equipment(accessory.name))
                    }
                  })
                }
              }
            }
          })
        }
      }
    } else {
      throw new Error(
        `Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
      )
    }

    return character
  }

  applyEnhancement(equipment, ore) {
    // Roll the dice and apply the enhancement on the equipment if it succeeds, then return back the equipment
    // Pretend it succeeded
    return { equipment, succeeded: true }
  }
}
Enter fullscreen mode Exit fullscreen mode

看起来我们的CharacterClassCreator.create方法变得有点复杂了。我们又违反了 DRY 原则。

但我们别无选择,因为把它放在 中没有意义Profile,而且我们也不想把它放进去,Game因为随着时间的推移,Game会有很多方法需要在高层级的作用域中运行。我们也不能直接把它硬编码到全局作用域中。那样会使我们的程序很容易出错。我们会污染全局作用域,并且代码的进一步扩展也必须涉及到全局作用域。

现在,它必须负责创建角色类,确保创建起始武器并将其附加到角色上,应用(如果有的话)用户通过会员资格购买的会员特权以配合他们的新角色,检查他们购买的配件类型(我们不要考虑我们的 MMORPG 在未来几年内理想情况下会有多少种不同类型的配件)以确保他们得到了他们所支付的东西(在这种情况下运行增强功能),将该增强附加到起始武器中,如果起始武器得到了增强,则更换起始武器,它甚至变得异步

如果我们把它发布成一个库会怎么样?现在每个开发人员的程序都会崩溃,因为我们把一个profile参数作为类的第一个参数,CharacterClassCreator并将其转换为异步的。

为了创建一个角色类别而必须做所有这些事情对于我们的类别来说太过繁重,CharacterClassCreator如下所示:

应用工厂模式之前的 JavaScript 错误代码

好吧,我们可以应用更多的工厂并委托创建这些处理其自身逻辑的对象的责任。

我将发布扩展代码并展示一个图表,说明在应用几个工厂解决其中一些问题时抽象的样子:

class Character {
  useEquipment() {}
}

class Mage extends Character {}
class Shaman extends Character {}
class Thief extends Character {}
class Archer extends Character {}
class Warrior extends Character {}

class Profile {
  constructor(name, email = '') {
    this.initializer = new ProfileInitializer()
    this.id = Math.random().toString(36).substring(2, 9)
    this.name = name
    this.email = email
  }

  async initialize() {
    await this.initializer.initialize(this)
  }

  synchronizeProfileContacts(anotherProfile) {
    // Do something to inherit anotherProfile's contacts
  }

  setName(name) {
    this.name = name
  }

  setEmail(email) {
    this.email = email
  }

  setCharacter(character) {
    this.character = character
  }

  setMembership(membership) {
    this.membership = membership
  }
}

class Equipment {
  constructor(name) {
    this.name = name
  }
}

class CharacterClassCreator {
  create(classType) {
    const creatorMap = {
      archer: {
        Class: Archer,
      },
      mage: {
        Class: Mage,
      },
      shaman: {
        Class: Shaman,
      },
      thief: {
        Class: Thief,
      },
      warrior: {
        Class: Warrior,
      },
    }

    let character

    if (creatorMap[classType]) {
      const { Class } = creatorMap[classType]
      character = new Class()
      return character
    } else {
      throw new Error(
        `Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
      )
    }
  }
}

class Membership {
  constructor(type) {
    this.type = type
  }

  async applyMembershipCode(profile, code) {
    // return fetch(`https://www.mymmorpg.com/api/v1/membership-code?code=${code}`)
    //   .then((resp) => resp.json())
    //   .then((data) => data)
    return { equipments: [{ type: 'ore' }] }
  }
}

class MembershipFactory {
  create(type) {
    const membership = new Membership(type)
    return membership
  }
}

class ProfileInitializer {
  constructor() {
    this.initializers = {}
  }

  async initialize(profile) {
    for (const [name, initialize] of Object.entries(this.initializers)) {
      const initialize = profile.initializers[name]
      await initialize(profile.character)
    }
    return profile.character
  }

  use(name, callback) {
    this.initializers[name] = callback
  }
}

class EquipmentEnhancer {
  applyEnhancement(equipment, ore) {
    // Roll the dice and apply the enhancement on the equipment if it succeeds, then return back the equipment
    // Pretend it succeeded
    return { equipment, succeeded: true }
  }
}

class Game {
  constructor() {
    this.users = {}
  }

  createUser(name) {
    const user = new Profile(name)
    this.users[user.id] = user
    return user
  }
}

;(async () => {
  const characterClassCreator = new CharacterClassCreator()
  const profileInitializer = new ProfileInitializer()
  const equipmentEnhancer = new EquipmentEnhancer()
  const membershipFactory = new MembershipFactory()

  const game = new Game()

  // Initializes the starter weapon
  profileInitializer.use(async (profile) => {
    let character = profile.character
    let starterWeapon

    if (character instanceof Archer) {
      starterWeapon = new Equipment('crossbow')
    } else if (character instanceof Mage) {
      starterWeapon = new Equipment('staff')
    } else if (character instanceof Shaman) {
      starterWeapon = new Equipment('claw')
    } else if (character instanceof Thief) {
      starterWeapon = [new Equipment('dagger'), new Equipment('dagger')]
    } else if (character instanceof Warrior) {
      starterWeapon = new Equipment('sword')
    }

    character.useEquipment(starterWeapon)
  })

  // Initializes membership perks
  profileInitializer.use(async (profile) => {
    const character = profile.character

    switch (profile.code) {
      case 12512: {
        // They bought a gold membership.
        // Ensure we apply any starter equipment enhancements they included with their membership when they went through the checkout process for creating new accounts
        const goldMembership = membershipFactory.create('gold')

        profile.setMembership(goldMembership)

        const { equipments: _equipments_ } =
          await profile.membership.applyMembershipCode(profile.code)
        // There are equipments provided in addition to the starter weapon.
        // This is most likely the result of the user paying for extra stuff
        _equipments_.forEach((equipment) => {
          // For thief class that uses duo daggers
          if (Array.isArray(equipment)) {
            character.useEquipment(equipment[0])
            character.useEquipment(equipment[1])
          } else {
            character.useEquipment(equipment)
          }

          if (profile.membership.accessories) {
            profile.membership.accessories.forEach(({ accessory }) => {
              if (accessory.type === 'ore') {
                // Each ore has an 80% chance of increasing the strength of some weapon when using some enhancer
                const { succeeded, equipment } =
                  equipmentEnhancer.applyEnhancement(starterWeapon, accessory)
                if (succeeded) starterWeapon = equipment
              } else if (accessory.type === 'fun-wear') {
                // They also bought something fancy just to feel really cool to their online friends
                character.useEquipment(new Equipment(accessory.name))
              }
            })
          }
        })
        break
      }
      default:
        break
    }
  })

  const bobsProfile = game.createUser('bob')
  // bobsProfile.code = 12512
  const bobsCharacter = await characterClassCreator.create('shaman')

  console.log(game)
  console.log(bobsProfile)
  console.log(bobsCharacter)
})()
Enter fullscreen mode Exit fullscreen mode

下面是它的视觉效果:

通过工厂设计模式结果降低 JavaScript 复杂性

我们现在可以清楚地看到,工厂已经抽象出一些更有意义的复杂性。

每个类对象都有各自的职责。在本文的示例中,我们主要关注的是配置文件的初始化,这是我们代码中最敏感的部分。我们希望配置文件保持简单,并允许工厂处理诸如应用哪些类型的成员资格以及它们的行为方式等抽象概念我们Profile 只需担心确保配置文件具有设置所有部分所需的接口

结论

感谢您的阅读,期待我将来发布更多优质帖子!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/the-power-of-factory-design-pattern-in-javascript-2bf8
PREV
JavaScript 中高阶函数的威力(附示例和用例)
NEXT
JavaScript 中复合模式的威力