基本概念
在给出工厂模式的定义之前,不妨先来了解一下工厂的概念。
通过百度百科查到的所谓工厂的定义:是一类用以生产货物的大型工业建筑物,即我们为工厂输送原料,经过工厂对原料进行处理加工之后会输出产物。
例如下面这样一个例子:
张三是一名大学生,毕业后为了上班方便就考虑买一台车,因此他找到一家汽车商城,对工作人员提出来品牌,颜色,价格等需求,随后汽车商城会根据张三的需求做一系列的工作。而对于张三而言,这些都是无需他考虑的,他仅仅提出需求,然后等待结果就好。
这实际上就是工厂的作用,你需要一辆汽车,可以直接从商城里提车,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
而工厂模式也是如此,在创建对象时不会对调用者暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
在下面的内容中,会带大家了解两种工厂模式:简单工厂模式和抽象工厂模式。
简单工厂模式
简单工厂模式又称为静态工厂方法模式,通过这一模式,我们可以分离构造过程中“变的部分”与“不变的部分”。
即对“变的部分”进行抽离封装,而专注“不变的部分”的改变。
在一所学校里,一般至少存在三种角色:校长,老师,学生。那么在前端里面我们就需要构建三个构造器(通过 ES6 中的类来实现),现在来分别构建这三种角色:
首先,我们在实验环境中新建 index.html
文件与 index.js
文件,并在 index.html
中引入 index.js
,就像这样:
随后,在 index.js
中添加校长、老师、学生这三种构造器:
// index.js
// 校长构造器
class Headmaster {
constructor(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = "headmaster";
}
job() {
return "管理学校";
}
}
// 老师构造器
class Teacher {
constructor(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = "teacher";
}
job() {
return "传授知识";
}
}
// 学生构造器
class Student {
constructor(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = "student";
}
job() {
return "学习知识";
}
}
随后在 index.js
中测试这三个构造器是否成功:(如何运行可以参照前言中的运行步骤)
// index.js
const zhangsan = new Headmaster("张三", 36, "man");
console.log("张三的任务是:", zhangsan.job()); // 张三的任务是: 管理学校
const luoxiang = new Teacher("罗翔", 24, "man");
console.log("罗翔的任务是:", luoxiang.job()); // 罗翔的任务是: 传授知识
const xiaohong = new Student("小红", 12, "woman");
console.log("小红的任务是:", xiaohong.job()); // 小红的任务是: 学习知识
可以看到,三个构造器都成功的返回了正确的任务。
在上面的构造器中,除了名字,年龄,性别这三个公共属性之外,还可以根据职业这个属性来分配不同的工作内容。
例如职业是校长(headmaster),那么他/她的工作就是管理学校,职业是老师(teacher),那么其工作就是传授知识。
学校开办一段时间之后,为了改善学校的卫生情况,校长决定增加清洁工这个职务,负责管理校园的清洁卫生,此时我们就需要再增加一个构造器:
// index.js
// 清洁工构造器
class Cleaner {
constructor(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = "cleaner";
}
job() {
return "打扫卫生";
}
}
同时来验证下其正确性:
// index.js
const lisi = new Cleaner("李四", 60, "man");
console.log("李四的任务是:", lisi.job()); // 李四的任务是: 打扫卫生
不出意外,同样返回了正确的工作任务。
此时,我们很轻松的就添加了一个清洁工构造器,如果你认为这就万事大吉那就错了。随着学校的学生越来越多,学校规模也会越来越大,因此不得不增加生活老师职位负责学生的生活方面,增加食堂师傅负责日常饮食,增加保安负责校园安全等等。
可以看到,在真实情况下的学校里,各种各样的职位非常多,难道我们都需要逐一的去添加相关的构造函数吗?
即使在极端情况下,我们真的添加了这么多构造器,我们在增加的时候不仅需要根据不同的职务去修改他们的工作内容,同时还要考虑职位对应的工作变动的情况,例如当保安人手不够时,需要生活老师去临时参加巡逻工作。这些都让我们不得不去频繁的改动构造器,这样的代价和实现方式显然不能让我们满意。
如何我们能只通过一个构造器就能完成上述工作,让我们回忆下前面提到的对简单工厂模式作用的描述:可以分离构造过程中“变的部分”与“不变的部分”。
那么是否能分离出上面大量构造器的共性呢?当然没问题,请思考一下,这些构造器中“不变的部分”,相信聪明的大家一定已经明白过来,就是其中的名字,年龄与性别这三个属性。
在 index.js
中继续。
第一步,用一个构造器来实现不同职务的工作分配:
// index.js
// 分配任务
class AssigningTask {
constructor() {}
// 静态方法:根据职务分配工作
static JobAssigning = (name, age, sex, career) => {
switch (career) {
case "headmaster":
return new Headmaster(name, age, sex);
case "teacher":
return new Teacher(name, age, sex);
case "student":
return new Student(name, age, sex);
case "cleaner":
return new Cleaner(name, age, sex);
default:
return null;
}
};
}
// 检验任务是否能正确分配
const zhangsan = AssigningTask.JobAssigning("张三", 36, "man", "headmaster");
console.log("张三的任务是:", zhangsan.job()); // 张三的任务是: 管理学校
注意:在同一个文件中,需要把之前命名的张三变量注释掉,否则会报错。
在第一步中,我们通过静态方法 JobAssigning 对职业(career)的区分,进而返回不同构造器的实例。在这种情况下,就只需要对一个 AssigningTask 进行操作。
但是在这一步中,我们并没有实现对共性的抽离,且仍然需要书写大量的构造器,无需着急,在下一步时会同时解决这两个问题。
这里需要新建一个文件 index2.js
,并引入 index.html
中:
第二步:抽离共性
// index2.js
// 抽离共性
class People {
constructor(name, age, sex, career, job) {
this.name = name;
this.age = age;
this.sex = sex;
this.career = career;
this.job = job;
}
jobFn = () => {
return this.job;
};
}
// 区分非共性
class AssigningTask {
constructor() {}
// 静态方法:根据职务分配工作
static JobAssigning = (name, age, sex, career) => {
let job = "";
switch (career) {
case "headmaster":
job = "管理学校";
break;
case "teacher":
job = "传授知识";
break;
case "student":
job = "学习知识";
break;
case "cleaner":
job = "打扫卫生";
break;
default:
job = "";
}
return new People(name, age, sex, career, job);
};
}
希望各位同学能自己在实验环境中测试一下该方法的结果。注意的是,这里 People
类中的工作方法是 jobFn
,注意在调用时不要搞错了。
在上面的 People
类中,我们抽离出来了名字,年龄这些共有属性,随后在 AssigningTask
中不再返回各种不一样的构造器,而是仅仅用职务字段来区分不同的工作内容,然后作为参数传入新建的 People
类中即可。
通过这样的方式,我们就避免了数据大量且重复的构造器,不仅仅简化了工作的复杂度,在代码的阅读性和维护上都有了很大的改进。
到了这一步,已经基本实现了目标:分离“变的部分”与“不变的部分”,通过传参的方式来在“工厂”内部处理,我们无需但是过程如何,而是只看结果就行。
抽象工厂模式
有些情况下,我们的确可以使用简单工厂模式来重构,但不一定是最好的方案,或者说还能有更好的实现。
本小节要介绍的抽象工厂模式,它不像简单工厂那样好理解,却拥有更高的抽取能力,复用能力及维护能力。
仍然用上面的学校的例子,现在我们已经用简单工厂进行了一次重构,实现了共性的抽离,专注于非共性的改变。
但是这儿仍然存在一些问题,例如:
- 在
AssigningTask
类中,静态方法JobAssigning
里面存在大量的判断分支,意味着如果我们有一百个岗位,那么就有可能要写一百个判断条件去区分不同的职务。这会让我们对JobAssigning
方法做很大的调整,且随时有可能调整。这一点就违背了“开放封闭”这一涉及原则。 - 目前只有数种共性,例如名字,年龄等,然而实际的场景下,人还具有更多的共性,例如身高,体重等等,那么如果要添加这些共性,那么过程一定是让人痛苦的,因为我们首先要在 People 类中添加,其次还要修改
AssigningTask
类中的JobAssigning
方法。这同样会违背“开放封闭”原则。
我们先举一个简单的例子:
新建一个 abstract.js
文件,同样引入 index.html
中。
构建第一个抽象类(人),它拥有着最高的共性,即对任何人而言都是存在的。
// abstract.js
/**
* 人的抽象类,包含了名称,年龄及吃这三个共性
*/
class PeopleAbstract {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log("人可以吃东西!");
}
work() {
console.log("人可以工作!");
}
}
人主要会分为男女,因此我们可以在构建 Man
和 Woman
这两个类,使用 extends
关键词实现对 PeopleAbstract
类的继承,就像这样:
// abstract.js
// 男人
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("女人的工作");
}
}
让我们看一下实际的效果:
// abstract.js
const man = new Man();
man.eat(); // 人可以吃东西!
man.work(); // 男人的工作
const woman = new Woman();
woman.eat(); // 人可以吃东西!
woman.work(); // 女人的工作
可以看到,Man
和 Woman
实例化的对象不仅可以调用到父类的 eat
方法,并且还成功调用了自身重写的 work
方法。
小结一下:在 Man
和 Woman
这两个子类中,不仅各自添加了属性,还重写了 work
方法,有没有感觉到一点方便的就是,我们对其中任意一个子类修改,都不会影响到另一个子类。
我们回到学校的例子,为了统一,这里把性别这一属性仍然与名字等放在一起,现在来重新构建 People
类:
继续在 abstract.js
中实现:
// abstract.js
class People {
constructor(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
jobFn = () => {};
}
可以看到,我们不再传入 job
与 career
这两个参数,同时在 jobFn
方法中也不再有任何具体的操作。这样就实现了一个“抽象”的类,只定义了参数和方法,而不参与具体的实现。
接下来,利用 extends
关键字就可以单独的实现校长类:
// abstract.js
class Headmaster extends People {
constructor(name, age, sex) {
super(name, age, sex);
}
jobFn = () => {
return `${this.name}的任务是:管理学校`;
};
}
来看看效果如何:
// abstract.js
const zhangsan = new Headmaster("张三", 36, "man", "headmaster");
console.log(zhangsan.jobFn()); // 张三的任务是:管理学校
从打印结果上可以看到,Headmaster
完全满足要求,这个时候我们就可以依次把其他几种职务依次实现一下:
// abstract.js
class Teacher extends People {
constructor(name, age, sex) {
super(name, age, sex);
}
jobFn = () => {
return `${this.name}的任务是:传授知识`;
};
}
class Student extends People {
constructor(name, age, sex) {
super(name, age, sex);
}
jobFn = () => {
return `${this.name}的任务是:学习知识`;
};
}
class Cleaner extends People {
constructor(name, age, sex) {
super(name, age, sex);
}
jobFn = () => {
return `${this.name}的任务是:打扫卫生`;
};
}
至此,整个例子就结束了,不知道各位同学到这里是否有新的感悟。
在抽象设计模式中,我们真正的将共性单独的抽离出去,并且仅仅做了属性与方法的定义而不参与具体实现,通过类继承的方式去分别实现了不同的职务类。并且当你对各个子类添加或修改时,是不会影响到父类及其他的子类的,这让后续的代码维护变得方便很多。
也许会有同学问,这样子和我们一开始分开定义各个职务不是一样的嘛。
当然不是,这里做一个简单的区分:
在开始的实现中,我们在每一个类中都写了名字,年龄这些属性,试想如果存在 100 个属性那就意味着需要在每个类中都要写 100 个属性,这个工作不仅繁复且后期不易维护。
而在抽象工厂的思想下,我们是通过 extends
这样继承的关系,仅仅需要通过 super
方法就能调用到所有父类的属性,因此只需要在父类中书写属性,而在子类中直接继承即可。这一点你完全可以从代码量上看出来。
让我们来总结一下抽象工厂:它可以把共有的属性抽出去形成抽象类,这个抽象类(People)不负责实例化,它仅仅用来对一些公有的属性或方法进行定义。具体的实例化由继承该抽象类的具体类去完成的。
工厂模式的应用
工厂模式的应用非常广泛,回到我们在基本概念中举到的张三买车的例子。
新建一个 car.js
文件,并引入 index.html
中。
首先就需要一个 car 的抽象类来定义车的共性:
// car.js
// 车的抽象类
class Car {
constructor(brand, color, price) {
this.brand = brand;
this.color = color;
this.price = price;
}
// 综合结论
conclusion = () => {};
}
为了方便举例,这里就取奔驰与比亚迪这两种为例:
// car.js
// 奔驰
class Benz extends Car {
constructor(brand, color, price, type) {
super(brand, color, price);
this.type = type;
}
conclusion = () => {
return `品牌:${this.brand},颜色:${this.color},价格:${this.price},它是一辆${this.type}`;
};
}
// 比亚迪
class Byd extends Car {
constructor(brand, color, price, type) {
super(brand, color, price);
this.type = type;
}
conclusion = () => {
return `品牌:${this.brand},颜色:${this.color},价格:${this.price},它是一辆${this.type}`;
};
}
此时根据张三的需求,我们可以给出两种车型:
// car.js
// 1、张三希望要一辆红色,价格30万,进口的奔驰车
const benz = new Benz("奔驰", "红色", "300000", "进口车");
console.log(benz.conclusion()); // 品牌:奔驰,颜色:红色,价格:300000,它是一辆进口车
// 2、张三希望要一辆黑色,价格35万,国产的比亚迪车
const byd = new Byd("比亚迪", "黑色", "350000", "国产车");
console.log(byd.conclusion()); // 品牌:比亚迪,颜色:黑色,价格:350000,它是一辆国产车
这类似的例子在生活中举不胜举,只要大家在理解工厂模式的基础上去细心发现,一定能发现很多工厂模式的应用。
实验挑战
在现代社会中,手机已经非常普遍,因此希望各位同学能借鉴前面的例子,利用工厂模式的思维对手机进行品牌的抽象实现。
要求:
- 分为苹果和安卓两个品牌,它们的共性包括品牌(brand)和尺寸(size);
- 苹果新增创始人(originator)属性,安卓新增发布时间(time)属性;
- 为它们添加 show 方法,通过打印方式输出描述内容。
希望能利用这个课后小例子加深大家对工厂模式的认识。
答案代码放在课程的源码包中的 phone.js
文件,大家可以自行下载,和自己的实现方式进行比对。( 源码下载地址 )
小结及下节实验内容预告
至此,关于工厂模式的介绍基本告一段落了,这里可以为大家学习提供两点建议:
- 了解一下 JAVA 中的抽象类,可以帮助我们理解这里的抽象工厂;( 可参考: JAVA 抽象类 )
- 从例子出发,自己从举例中剥离出抽象类的定义;
下一节实验会为大家带来的是单例模式,有时间的同学可以先自行的去简单了解一下什么是单例模式,而单例模式在前端中的应用有哪些。相信通过提前的预习和对问题的思考,下一节实验一定会学习的更加轻松,也更容易理解。
标签:name,创建,age,js,sex,job,工厂,设计模式,class From: https://www.cnblogs.com/xzemt/p/18030129