工厂函数
实现工厂模式的其中一种方法就是使用工厂函数。
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,来让我们受益更多。但同时也要注意到这样会增加代码的复杂度。