JavaScript 设计模式#2:工厂模式及其在 TypeScript 中的应用

709 阅读3分钟

工厂函数

实现工厂模式的其中一种方法就是使用工厂函数。

const createPerson = ({
  firstName,
  lastName
}) => ({
  firstName,
  lastName,
  speak: () => console.log(`My name is ${firstName} ${lastName}`)
});
 
const person = createPerson({
  firstName: 'John',
  lastName: 'Smith'
});
 
console.log(person.speak()); // My name is John Smith

工厂函数最终返回一个对象。也可以使用混合的方式来复用工厂函数。

const withNames = ({
  firstName,
  lastName
}) => ({
  firstName,
  lastName,
  speak: () => console.log(`My name is ${firstName} ${lastName}`)
});
 
const createProgrammer = ({
  firstName,
  lastName,
  programmingLanguage
}) => ({
  ...withNames({ firstName, lastName }),
  programmingLanguage
});
 
const createTeacher = ({
  firstName,
  lastName,
  subject
}) => ({
  ...withNames({ firstName, lastName }),
  subject
});
const programmer = createProgrammer({
  firstName: 'John',
  lastName: 'Smith',
  programmingLanguage: 'JavaScript'
});
 
const teacher = createTeacher({
  firstName: 'Jacob',
  lastName: 'Williams',
  subject: 'Maths'
});

我们也可以通过类或者继承来实现同样的结果,一些主流的第三方库或者框架,比如 Angular 和 React 都采用了类,所以熟悉上述方式是有必要的。

工厂方法模式

工厂方法设计模式是基本的设计模式之一,它为创建对象提供一种方法,而不需要指定要创建的对象的确切类。

想象我们要建立一个在线编程教学系统,首先创建一个老师的类。

class Teacher {
  constructor(name, programmingLanguage) {
    this.name = name;
    this.programmingLanguage = programmingLanguage;
  }
}

看上去还不错,但是上述方法并不具有扩展性。比如我们需要扩展一个音乐老师的类型,就是如下写法:

const TEACHER_TYPE = {
  CODING: 'coding',
  MUSIC: 'music'
};
 
class Teacher {
  constructor(type, name, instrument, programmingLanguage) {
    this.name = name;
    if (type === TEACHER_TYPE.CODING) {
      this.programmingLanguage = programmingLanguage;
    } else if (type === TEACHER_TYPE.MUSIC){
      this.instrument = instrument;
    }
  }
}

正如所见,老师类变得非常臃肿难懂,如果添加更多老师类型,情况会变得更加糟糕。解决以上问题的办法就是使用工厂方法模式。

const TEACHER_TYPE = {
  CODING: 'coding',
  MUSIC: 'music'
};
class CodingTeacher {
  constructor(properties) {
    this.name = properties.name;
    this.programmingLanguage = properties.programmingLanguage;
  }
}
 
class MusicTeacher {
  constructor(properties) {
    this.name = properties.name;
    this.instrument = properties.instrument;
  }
}
 
class TeacherFactory {
  static getTeacher(type, properties) {
    if (type === TEACHER_TYPE.CODING) {
      return new CodingTeacher(properties);
    } else if (type === TEACHER_TYPE.MUSIC) {
      return new MusicTeacher(properties);
    }
  }
}

如上,我们可以使用 TeacherFactory 创建任意类型的老师,同时不会破坏任何代码。

const codingTeacher = TeacherFactory.getTeacher(TEACHER_TYPE.CODING, {
  programmingLanguage: 'JavaScript',
  name: 'John'
});
 
const musicTeacher = TeacherFactory.getTeacher(TEACHER_TYPE.MUSIC, {
  instrument: 'Guitar',
  name: 'Andy'
});

这里直接通过类名来调用 getTeacher 方法。我们可以通过类的继承让代码更清晰一些。

class Teacher {
  constructor(properties) {
    this.name = properties.name;
  }
}
 
class CodingTeacher extends Teacher {
  constructor(properties) {
    super(properties);
    this.programmingLanguage = properties.programmingLanguage;
  }
}
 
class MusicTeacher extends Teacher {
  constructor(properties) {
    super(properties);
    this.instrument = properties.instrument;
  }
}

我们可以将类似功能放到一个单独的类中,减少 bug 的出现。在选择错误类型的时候抛出错误是一个不错的主意,我们使用 switch 语句来实现。

class TeacherFactory {
  static getTeacher(type, properties) {
    switch (type) {
      case TEACHER_TYPE.CODING:
        return new CodingTeacher(properties);
      case TEACHER_TYPE.MUSIC:
        return new MusicTeacher(properties);
      default:
        throw new Error('Wrong teacher type chosen');
    }
  }
}

通过这层抽象,我们就不需要在 constructor 中做任何修改,这样就可以在不改动已有类型的基础上,对应用做一些扩展。

工厂方法的 TypeScript 写法

使用 TypeScript 对工厂方法模式做一些类型检查会更好一些。

enum TEACHER_TYPE {
  CODING = 'coding',
  MUSIC = 'music',
}
interface TeacherProperties {
  name: string;
}
class Teacher {
  public name: string;
  constructor(properties: TeacherProperties) {
    this.name = properties.name;
  }
}
interface CodingTeacherProperties {
  name: string;
  programmingLanguage: string;
}
class CodingTeacher extends Teacher {
  public programmingLanguage: string;
  constructor(properties: CodingTeacherProperties) {
    super(properties);
    this.programmingLanguage = properties.programmingLanguage;
  }
}
interface MusicTeacherProperties {
  name: string;
  instrument: string;
}
class MusicTeacher extends Teacher {
  public instrument: string;
  constructor(properties: MusicTeacherProperties) {
    super(properties);
    this.instrument = properties.instrument;
  }
}

使用 TypeScript 后,我们需要用一个特定的对象来指定对应的属性具体是什么类型。为了做到这点,可以使用方法重载。

class TeacherFactory {
  public static getTeacher(type: TEACHER_TYPE.MUSIC, properties: MusicTeacherProperties): MusicTeacher;
  public static getTeacher(type: TEACHER_TYPE.CODING, properties: CodingTeacherProperties): CodingTeacher;
  public static getTeacher(type: TEACHER_TYPE, properties: MusicTeacherProperties & CodingTeacherProperties) {
    switch (type) {
      case TEACHER_TYPE.CODING:
        return new CodingTeacher(properties);
      case TEACHER_TYPE.MUSIC:
        return new MusicTeacher(properties);
      default:
        throw new Error('Wrong teacher type chosen');
    }
  }
}

如上,当新建音乐老师时候,就需要传音乐老师的一些属性,否则,TypeScript 编译器会抛出错误。

const codingTeacher = TeacherFactory.getTeacher(TEACHER_TYPE.CODING, {
  programmingLanguage: 'JavaScript',
  name: 'John',
});
 
const musicTeacher = TeacherFactory.getTeacher(TEACHER_TYPE.MUSIC, {
  instrument: 'Guitar',
  name: 'Andy',
});

有了类型检查,TypeScript 现在知道了 codingTeacher

console.log(codingTeacher.instrument);

error TS2339: Property ‘instrument’ does not exist on type ‘CodingTeacher’.

总结

在这篇文章里,我们介绍了工厂方法模式是什么,以及了解了工厂函数。同时学习了工厂模式如何结合 TypeScript,来让我们受益更多。但同时也要注意到这样会增加代码的复杂度。

参考:Javascript 设计模式