JS `Factory Pattern`:根据条件创建不同类型的对象

各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊JavaScript里的“工厂模式”。这玩意儿听起来像个流水线,实际上也差不多,只不过流水线上生产的是实体商品,而我们这里生产的是JavaScript对象。

为啥要用工厂模式?

话说回来,直接new一个对象不香吗?为啥要搞这么复杂?答案是:为了解耦,为了更灵活,为了让你的代码更健壮,更易于维护。

想象一下,你正在开发一个游戏,里面有很多种怪物:史莱姆、哥布林、骷髅兵等等。每个怪物都有自己的属性和行为。如果你直接在代码里new Slime()new Goblin()new Skeleton(),那代码就会变得非常冗余,而且以后如果想新增一种怪物,或者修改某种怪物的创建方式,那就要改动很多地方,想想就头疼。

工厂模式就是来解决这个问题的。它可以把对象的创建逻辑封装起来,让你只需要告诉工厂“我想要一个史莱姆”,工厂就会帮你创建一个史莱姆对象,而你不需要关心史莱姆是怎么被创建的。

工厂模式的几种实现方式

工厂模式有很多种实现方式,咱们今天主要讲三种:简单工厂模式、工厂方法模式和抽象工厂模式。

1. 简单工厂模式(Simple Factory Pattern)

这是最简单的一种工厂模式,也叫静态工厂模式。它只有一个工厂类,根据传入的参数来决定创建哪个对象。

// 怪物基类
class Monster {
  constructor(name, health, attack) {
    this.name = name;
    this.health = health;
    this.attack = attack;
  }

  attackTarget(target) {
    console.log(`${this.name} attacks ${target.name} for ${this.attack} damage!`);
    target.health -= this.attack;
    if(target.health <= 0){
      console.log(`${target.name} is defeated!`)
    }
  }
}

// 史莱姆类
class Slime extends Monster {
  constructor() {
    super('Slime', 50, 5);
  }

  split() {
    console.log('Slime splits into two smaller slimes!');
  }
}

// 哥布林类
class Goblin extends Monster {
  constructor() {
    super('Goblin', 80, 10);
  }

  steal(target) {
    console.log(`Goblin steals from ${target.name}!`);
  }
}

// 骷髅兵类
class Skeleton extends Monster {
  constructor() {
    super('Skeleton', 100, 15);
  }

  rattle() {
    console.log('Skeleton rattles its bones!');
  }
}

// 简单工厂类
class SimpleMonsterFactory {
  static createMonster(type) {
    switch (type) {
      case 'slime':
        return new Slime();
      case 'goblin':
        return new Goblin();
      case 'skeleton':
        return new Skeleton();
      default:
        throw new Error('Invalid monster type!');
    }
  }
}

// 使用工厂创建怪物
const slime = SimpleMonsterFactory.createMonster('slime');
const goblin = SimpleMonsterFactory.createMonster('goblin');
const skeleton = SimpleMonsterFactory.createMonster('skeleton');

slime.attackTarget(goblin);
goblin.attackTarget(skeleton);

console.log(goblin);
console.log(skeleton);

在这个例子中,SimpleMonsterFactory就是一个简单工厂类。它有一个静态方法createMonster,根据传入的type参数来创建不同类型的怪物对象。

优点:

  • 简单易懂,容易实现。
  • 将对象的创建逻辑集中到一个地方,方便管理。

缺点:

  • 违反了开闭原则(Open/Closed Principle),如果要新增一种怪物,就需要修改工厂类的代码。
  • 工厂类的职责过重,既要负责创建对象,又要负责判断创建哪个对象。

2. 工厂方法模式(Factory Method Pattern)

为了解决简单工厂模式的缺点,我们可以使用工厂方法模式。工厂方法模式定义了一个抽象工厂类,其中声明了一个抽象的工厂方法,用于创建对象。具体的工厂类实现这个抽象方法,创建具体的对象。

// 抽象怪物工厂类
class MonsterFactory {
  createMonster() {
    throw new Error('This method must be implemented by subclasses.');
  }
}

// 史莱姆工厂类
class SlimeFactory extends MonsterFactory {
  createMonster() {
    return new Slime();
  }
}

// 哥布林工厂类
class GoblinFactory extends MonsterFactory {
  createMonster() {
    return new Goblin();
  }
}

// 骷髅兵工厂类
class SkeletonFactory extends MonsterFactory {
  createMonster() {
    return new Skeleton();
  }
}

// 使用工厂创建怪物
const slimeFactory = new SlimeFactory();
const slime = slimeFactory.createMonster();

const goblinFactory = new GoblinFactory();
const goblin = goblinFactory.createMonster();

const skeletonFactory = new SkeletonFactory();
const skeleton = skeletonFactory.createMonster();

slime.attackTarget(goblin);
goblin.attackTarget(skeleton);

console.log(goblin);
console.log(skeleton);

在这个例子中,MonsterFactory是一个抽象工厂类,它定义了一个抽象方法createMonsterSlimeFactoryGoblinFactorySkeletonFactory是具体的工厂类,它们分别实现了createMonster方法,用于创建不同类型的怪物对象。

优点:

  • 符合开闭原则,如果要新增一种怪物,只需要新增一个具体的工厂类即可,不需要修改已有的代码。
  • 每个工厂类只负责创建一种对象,职责单一。

缺点:

  • 类的数量会增多,代码会变得比较复杂。

3. 抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式是工厂方法模式的进一步抽象。它定义了一个抽象工厂接口,用于创建一系列相关或相互依赖的对象。每个具体的工厂类实现这个接口,创建一组相关的对象。

想象一下,你的游戏现在有了不同的难度级别:简单、普通和困难。每个难度级别都有自己的一套怪物。简单级别的怪物比较弱,困难级别的怪物比较强。

// 抽象怪物工厂接口
class AbstractMonsterFactory {
  createSlime() {
    throw new Error('This method must be implemented by subclasses.');
  }

  createGoblin() {
    throw new Error('This method must be implemented by subclasses.');
  }

  createSkeleton() {
    throw new Error('This method must be implemented by subclasses.');
  }
}

// 简单难度怪物工厂
class EasyMonsterFactory extends AbstractMonsterFactory {
  createSlime() {
    return new Slime('Easy Slime', 30, 3); // 更弱的史莱姆
  }

  createGoblin() {
    return new Goblin('Easy Goblin', 60, 8); // 更弱的哥布林
  }

  createSkeleton() {
    return new Skeleton('Easy Skeleton', 80, 12); // 更弱的骷髅兵
  }
}

// 普通难度怪物工厂
class NormalMonsterFactory extends AbstractMonsterFactory {
  createSlime() {
    return new Slime();
  }

  createGoblin() {
    return new Goblin();
  }

  createSkeleton() {
    return new Skeleton();
  }
}

// 困难难度怪物工厂
class HardMonsterFactory extends AbstractMonsterFactory {
  createSlime() {
    return new Slime('Hard Slime', 80, 8); // 更强的史莱姆
  }

  createGoblin() {
    return new Goblin('Hard Goblin', 120, 15); // 更强的哥布林
  }

  createSkeleton() {
    return new Skeleton('Hard Skeleton', 150, 20); // 更强的骷髅兵
  }
}

// 修改Monster类以接受自定义的name,health,attack
class Monster {
  constructor(name, health, attack) {
    this.name = name;
    this.health = health;
    this.attack = attack;
  }

  attackTarget(target) {
    console.log(`${this.name} attacks ${target.name} for ${this.attack} damage!`);
    target.health -= this.attack;
    if(target.health <= 0){
      console.log(`${target.name} is defeated!`)
    }
  }
}

// 史莱姆类
class Slime extends Monster {
  constructor(name = 'Slime', health = 50, attack = 5) {
    super(name, health, attack);
  }

  split() {
    console.log('Slime splits into two smaller slimes!');
  }
}

// 哥布林类
class Goblin extends Monster {
  constructor(name = 'Goblin', health = 80, attack = 10) {
    super(name, health, attack);
  }

  steal(target) {
    console.log(`Goblin steals from ${target.name}!`);
  }
}

// 骷髅兵类
class Skeleton extends Monster {
  constructor(name = 'Skeleton', health = 100, attack = 15) {
    super(name, health, attack);
  }

  rattle() {
    console.log('Skeleton rattles its bones!');
  }
}

// 使用工厂创建怪物
const easyFactory = new EasyMonsterFactory();
const easySlime = easyFactory.createSlime();
const easyGoblin = easyFactory.createGoblin();
const easySkeleton = easyFactory.createSkeleton();

const normalFactory = new NormalMonsterFactory();
const normalSlime = normalFactory.createSlime();
const normalGoblin = normalFactory.createGoblin();
const normalSkeleton = normalFactory.createSkeleton();

const hardFactory = new HardMonsterFactory();
const hardSlime = hardFactory.createSlime();
const hardGoblin = hardFactory.createGoblin();
const hardSkeleton = hardFactory.createSkeleton();

console.log(easySlime);
console.log(normalSlime);
console.log(hardSlime);

easySlime.attackTarget(normalGoblin);
normalGoblin.attackTarget(hardSkeleton);

console.log(normalGoblin);
console.log(hardSkeleton);

在这个例子中,AbstractMonsterFactory是一个抽象工厂接口,它定义了创建史莱姆、哥布林和骷髅兵的方法。EasyMonsterFactoryNormalMonsterFactoryHardMonsterFactory是具体的工厂类,它们分别实现了AbstractMonsterFactory接口,用于创建不同难度级别的怪物。

优点:

  • 可以创建一组相关的对象,保证它们之间的一致性。
  • 符合开闭原则,如果要新增一种难度级别,只需要新增一个具体的工厂类即可,不需要修改已有的代码.

缺点:

  • 类的数量会更多,代码会变得更加复杂。
  • 如果要新增一种新的怪物类型,需要修改所有的抽象工厂和具体工厂类,维护成本较高。

三种工厂模式的对比

为了方便大家理解,我把这三种工厂模式的特点总结在一个表格里:

特点 简单工厂模式 工厂方法模式 抽象工厂模式
优点 简单易懂,容易实现 符合开闭原则,职责单一 可以创建一组相关的对象,保证它们之间的一致性,符合开闭原则
缺点 违反开闭原则,工厂类的职责过重 类的数量会增多,代码会变得比较复杂 类的数量会更多,代码会变得更加复杂,如果要新增一种新的怪物类型,维护成本较高
适用场景 对象类型比较少,对象的创建逻辑比较简单 对象类型比较多,需要灵活扩展 需要创建一组相关的对象,并且需要灵活扩展
是否易于扩展 较差 较好 更好

选择哪种工厂模式?

选择哪种工厂模式取决于你的具体需求。一般来说,如果对象类型比较少,对象的创建逻辑比较简单,那么可以选择简单工厂模式。如果对象类型比较多,需要灵活扩展,那么可以选择工厂方法模式。如果需要创建一组相关的对象,并且需要灵活扩展,那么可以选择抽象工厂模式。

工厂模式的应用场景

除了上面提到的游戏开发,工厂模式还可以在很多其他场景中使用,比如:

  • GUI框架: 可以使用工厂模式来创建不同类型的控件,比如按钮、文本框、下拉列表等等。
  • 数据库访问: 可以使用工厂模式来创建不同类型的数据库连接,比如MySQL连接、Oracle连接、SQL Server连接等等。
  • 日志记录: 可以使用工厂模式来创建不同类型的日志记录器,比如控制台日志记录器、文件日志记录器、数据库日志记录器等等。

总结

工厂模式是一种非常有用的设计模式,它可以帮助我们解耦代码,提高代码的灵活性和可维护性。虽然学习成本可能稍微高一些,但是一旦掌握了它,就能让你的代码更加优雅和健壮。希望今天的讲解能够帮助大家更好地理解和使用工厂模式。

最后,记住,设计模式不是银弹,不要为了使用模式而使用模式。只有在真正需要解决问题的时候,才应该考虑使用设计模式。好了,今天的讲座就到这里,谢谢大家!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注