设计模式创建型之工厂模式
什么叫工厂模式,这肯定是每一个人最开始的疑问。
那么为什么不先从它的名字来分析一下呢?
从百度百科查来的的所谓工厂的定义:是一类用以生产货物的大型工业建筑物。即我们为工厂输送原料,经过工厂对原料进行处理加工之后会输出产物。
而我们的工厂模式也正是这样,只不过这里的工厂变成了”代码工厂“。
这里提到的工厂的定义,大家现在不用太过在意,当阅读完本章全部内容后,再回过头来品味这一过程,一定会有更深的体会。
1.简单工厂模式
把逻辑中“不变的部分”与“变化的部分”进行分离
我们举一个最常用的例子,一个公司的部门有多个员工:
1、这些员工都有相同的地方,比如都有名字,都有年龄,都有性别等等,这些是他们所拥有的共性,也就是“不变的部分”。
那么我们可以使用一个构造器(构造函数)来实现,就像这样:
function Employee(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
cosnt employee1 = new Employee("小明", 25, "男");
cosnt employee2 = new Employee("小华", 24, "男");
cosnt employee3 = new Employee("小花", 21, "女");
// ...
好,我们现在有一个构造器实现了一个员工的共性(不变的部分)。
接下来我们来看看员工间的区别,也就是“变的部分”。在一个部门中,每一个员工都有自己的职位,不同的职位又有不同的工作内容。这时候我们应该怎么做呢?来看看下面的方法吧。
假设现在有开发同学(Developer),测试同学(Tester),产品同学(Producter)三类:
方法一:直接新增对应构造器 ,就像这样
function Developer(name, age, sex, career) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = career;
this.work = ['写bug', '改bug', 'diss 产品同学和测试同学'];
}
function Tester(name, age, sex, career) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = career;
this.work = ['测bug', '提bug', 'diss 开发同学和产品同学'];
}
function Producter(name, age, sex, career) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = career;
this.work = ['提需求', '验需求', 'diss 开发同学和测试同学'];
}
方法二:通过“工厂模式”思维来处理,就像这样
function Employee(name, age, sex, career, work) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = career;
this.work = work;
}
function departmentFactory(name, age, sex, career, work) {
let work;
switch(career) {
case 'Developer':
work = ['写bug', '改bug', 'diss 产品同学和测试同学'];
break;
case 'Tester':
work = ['测bug', '提bug', 'diss 开发同学和产品同学'];
break;
case 'Producter':
work = ['提需求', '验需求', 'diss 开发同学和测试同学'];
break;
default":
// ...
}
return new Employee(name, age, sex, career, work);
}
可以看到,当一个部门的工种比较少时,方法一当然不失为一种有效方法。然后在实际生活中,一个部门的工种是很多的,如果采用方法一,我们不仅要新增大量的构造函数,而且在使用 new 来构建时还需要思考当前这个人的职位对应哪些工作(work),这无疑是一个耗时耗力且不讨好的工作。
在方法二中运用了“工厂模式”思维,把不变的部分(名称,年龄,性别,职位)与变化的部分(工作内容)分离开来。我们无须再构造大量的构造器, 也不需要思考职位对应的工作内容。只需要简单的调用 departmentFactory 这个工厂,传入对应地参数(名称,年龄,性别,职位),工厂就会自动为我们分配工作内容。
从代码上看,我们的代码逻辑仿佛变复杂了,这仅仅是因为这是一个简单的例子,实际应用中的问题会复杂的多。试想一下,当你有数十个工种的时候,使用方法二中的工厂模式是多舒服的一件事。用现在很流行的一个词,你会觉得”真香!“。
最后简单小结一下吧,我们说,把大量构造器进行封装成一个”工厂“,让这个工厂更加有益于产出,这就是简单工厂模式。因此,在出现需要大量构造器的地方,我们可以考虑简单工厂模式来优化。
2.抽象工厂模式
如果大家有 Java 的知识储备,那么很容易发现抽象工厂其实就是 Java 中抽象类的表现。不过即使没有学习过 Java 也没有关系,本小节会尽最大努力帮助你理解抽象工厂模式。
让我们一起来考虑这样一个例子。
在现在社会中,每个人都会有名字,年龄,性别及吃东西的本能,那么我们会有这样一个构造器:
class People {
constructor (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
eat() {
console.log("人可以吃东西!");
}
}
这个时候,我们想区分男人和女人了,那么我们可以新增两个构造器:
class Man {
constructor (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.beard = "beard";
}
eat() {
console.log("人可以吃东西!");
}
work() {
console.log("男人主要负责体力活!");
}
}
class Woman {
constructor (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.hair = "long hair";
}
eat() {
console.log("人可以吃东西!");
}
work() {
console.log("女人主要负责家庭生活!");
}
}
可以看到,男人和女人除了有人类”固有的属性“之外,男人一般长浓密的胡须,女人一般有长长的头发,男人和女人都需要工作,但是工作的内容不同。
这里仅仅出现了两类区划,那么更多情况呢?
从年龄大小方面出发,我们还可以分为婴儿,青年,成年,老人等等;从工作内容上可以分为测试同学,产品同学,开发同学,研发经理,主管等等。
从不同的角度去分能有非常多的分支,难道在每一个新增的构造器里我们都需要不停的去重复声明类似于名字,性别,年龄这些人类固有的属性吗?
答案当然是否定的。也许这时候你会想,能不能使用简单工厂模式去优化呢?当然可以的,但是实际处理时你会发现,这种情况下使用简单工厂时,内部的判断逻辑会十分的庞大复杂,你需要用条件语句去不停的区分各种条件,就像这样:
function People(name, age, sex, bread, hair, work) {
this.name = name;
this.age = age;
this.sex = sex;
this.beard = bread;
this.hair = hair;
this.work = work;
function eat() {
console.log("人可以吃东西!");
}
}
function PeopleFactory(name, age, sex) {
let beard;
let hair;
let work;
switch (sex) {
case 'man':
beard = "beard";
work = () => { console.log("男人主要负责体力活!"); };
break;
case 'woman':
hair = "long hair";
work = () => { console.log("女人主要负责家庭生活!"); };
break;
default:
// ...
}
return new People(name, age, sex, bread, hair, work)
}
这里仅仅是区分男人和女人就会增加一大段条件判断,可想而知,如果需要再增加其他的分类,我们的简单工厂会有多庞大。
并且这样处理也对于后期维护十分不友好,当你在 People 中新增一些属性或者方法,不仅会导致代码体积越来越庞大,不易后期维护;甚至会影响到现有功能。
在前言中我们提到过设计原则,其中开放封闭原则说明:对拓展开放,对修改封闭。简单来讲,就是我们支持扩展代码,而不是不停的修改原代码。
为了解决上面出现的问题,在这里我们用抽象模式来改写上面的逻辑。
class PeopleAbstract {
constructor (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
eat() {
console.log("人可以吃东西!");
}
work() {
console.log("人可以工作!");
}
}
class Man extends PeopleAbstract {
constructor(name, age, sex, beard) {
super(name, age, sex);
this.beard = beard;
}
work = () => {
console.log("男人主要负责体力活!");
}
}
class Woman extends PeopleAbstract {
constructor(name, age, sex, hair) {
super(name, age, sex);
this.hair = hair;
}
work = () => {
console.log("女人主要负责家庭生活!");
}
}
1、可以看到我们现在拥有 PeopleAbstract 这个抽象类,通过 extends 关键字可以实现继承 PeopleAbstract 中的属性与方法。
2、在 PeopleAbstract 中我们已经定义了 work 方法,但在 Man 类里面我们可以定义自己专属的work方法。
3、我们只会用 Man 和 Woman 这样的实现类来直接生成实例。而抽象类 PeopleAbstract 中用来定义一些固有的属性与方法。
我们用 Man 来试验一下:
const man = new Man("张三", 24, "男", "long beard");
man.eat(); // 人可以吃东西!
man.work(); // 男人主要负责体力活!
好,现在如果要求从年龄方向来区分人,可以分为婴儿,青年,成年,老人:
class Baby extends PeopleAbstract {
// ...
}
class Teenager extends PeopleAbstract {
// ...
}
class Adult extends PeopleAbstract {
// ...
}
class oldMan extends PeopleAbstract {
// ...
}
可以看到,我们无需再书写大量重复的公共属性,而集中于不同分类下各自的特有属性。
这个例子实际上就是一个抽象工厂的简单应用,通过抽象工厂,我们把共有的属性抽出去形成抽象类,这个抽象类(PeopleAbstract)不负责实例化,它仅仅用来对一些公有的属性或方法进行定义。具体的实例化由继承该抽象类的具体类去完成的。
至此,关于两种工厂模式就已经介绍完了,相信大家可以看出来,文章并不突出工厂本身的定义概念,而是希望用生动的例子去刺激大家思考。
如果有介绍不清楚的地方,或者有错误的地方,欢迎大家给我留言!
最后,可以给大家一些建议:
1、可以了解一下 JAVA 中的抽象类,可以帮助我们理解这里的抽象工厂;
2、从例子出发,自己从举例中剥离出抽象类的定义;
3、不拘泥于细节,如果对哪里不明白,苦思冥想也解决不了,为什么不先放下,继续学习后面的内容,通过触类旁通,回过头来再看这一部分可能会有不一样的结果。