心游于艺,道不远人

变继承关系为组合关系

State 模式

继承描述了 is-a 的关系,子类可以继承父类的成员变量和函数,也可以修改父类的成员变量和函数。使用设计模式来实现代码复用,而不是使用继承实现代码复用。

State设计模式

它的核心思想是通过改变对象的内部状态来改变对象的行为,而无需修改对象本身的类结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 状态接口
interface Role {
void doWork();
}

// 具体状态
class Engineer implements Role {
public void doWork() { System.out.println("写代码"); }
}

class Manager implements Role {
public void doWork() { System.out.println("开会"); }
}

// 上下文类(Employee)
class Employee {
private Role role; // 当前状态

void setRole(Role role) { this.role = role; }
void doWork() { role.doWork(); } // 行为委托给状态对象
}

// 使用
Employee emp = new Employee();
emp.setRole(new Engineer());
emp.doWork(); // 输出 "写代码"

emp.setRole(new Manager());
emp.doWork(); // 输出 "开会"

装饰器模式

java 中有一个 Runnable 接口:

1
2
3
interface Runnable{
void run();
}

如何实现 LoggingRunnable,TransactionRunnable?

原始的 CodingTask 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CodingTask implements Runnable{

@Override
public void run() {
System.out.println("writing code");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// 抛出运行时异常防止异常被吞没
throw new RuntimeException(e);
}
}
}

一种不太好的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
abstract class LoggingRunnable implements Runnable{

protected abstract void doRun();

@Override
public void run() {
System.out.println("Task started at " + LocalTime.now());
doRun();
System.out.println("Task done at " + LocalTime.now());
}
}

abstract class TransactionRunnable implements Runnable{

protected abstract void doRun();

@Override
public void run() {
System.out.println("begin transaction.");
boolean shouldRollback = false;
try {
doRun();
} catch (Exception e) {
shouldRollback = true;
throw e;
} finally {
if (shouldRollback) {
System.out.println("rollback");
} else {
System.out.println("commit");
}
}
}
}

class CodingTask extends LoggingRunnable{

@Override
protected void doRun() {
System.out.println("writing code");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// 抛出运行时异常防止异常被吞没
throw new RuntimeException(e);
}
}
}

public class Test{
public static void main(String[] args) {
new CodingTask().run();
}
}

上面的代码虽然实现了功能,但是 CodingTask 的定义却变得不是很明朗:

  • CodingTask 从字面意思上是任务,原始的 CodingTask 实现了 Runnable 接口含义更明确
  • 引入了另外一个前提,凡是在使用 CodingTask 的地方必须记录日志
  • 如果我们现在不想要 CodingTask 记录日志了,而是当做一个 Transaction 处理,就必须修改代码让 CodingTask 继承 TransactionRunnable 抽象类
  • 如果 CodingTask 要同时完成记录日志和启用事务,由于单继承的局限性,上面的代码无法做到(即使支持多继承,由于 LoggingRunnable 和 TransactionRunnable 都有 doRun,到底调用哪个也是很复杂的)
  • run 方法比 doRun 具有更好的语义

装饰器模式可以解除上述耦合:

装饰器模式

类里面的成员要么是 public 的,要么是 private 的,如果遇到 protected 要反思下是否进行了正确建模。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class LoggingRunnable implements Runnable {

private final Runnable runnable;

public LoggingRunnable(Runnable runnable) {
this.runnable = runnable;
}

@Override
public void run() {
System.out.println("Task started at " + LocalTime.now());
runnable.run();
System.out.println("Task done at " + LocalTime.now());
}
}

class TransactionRunnable implements Runnable {

private final Runnable runnable;

public TransactionRunnable(Runnable runnable) {
this.runnable = runnable;
}

@Override
public void run() {
System.out.println("begin transaction.");
boolean shouldRollback = false;
try {
runnable.run();
} catch (Exception e) {
shouldRollback = true;
throw e;
} finally {
if (shouldRollback) {
System.out.println("rollback");
} else {
System.out.println("commit");
}
}
}
}

class CodingTask implements Runnable {

@Override
public void run() {
System.out.println("writing code");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

public class Test {
public static void main(String[] args) {

CodingTask task = new CodingTask();

task.run(); // 单独run
new LoggingRunnable(task).run(); // 记录日志,run
new LoggingRunnable(new TransactionRunnable(task)).run(); // 记录日志,开启事务,run
}
}

上面的代码,每个类各司其职,但是可以通过嵌套灵活组装。里层的逻辑装饰了外层逻辑。java 流就使用了装饰器模式。

一种快速出错的常规编程实践提倡一旦出错就立刻抛出异常,使错误的定位更加容易(这和忽略错误并将异常推迟到以后处理的方式截然相反)。

模拟接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const assert = require('assert');

class Interface {
constructor(name, methods) {
assert(name, '接口名不能为空');
assert(Array.isArray(methods) && methods.every(m => typeof m === 'string'), '请检查方法名数组');
this.name = name;
this.methods = methods;
}
static ensureImplements(obj, ...interfaces) {
for (const inter of interfaces) {
const clazz = inter.constructor;
const className = clazz.name;
assert.equal(clazz, Interface, `${className}必须通过new Interface生成`);
for (const methodName of inter.methods) {
assert.equal(typeof obj[methodName], 'function', `${className}没有实现呢${methodName}方法`);
}
}
}
}

const i1 = new Interface('map', ['zoomIn', 'zoomOut']);
const i2 = new Interface('show', ['show']);

// mock instance
const map = {
zoomIn() {
console.log('map zoom in');
},
zoomOut() {

}
};

Interface.ensureImplements(map, i1, i2);
map.zoomIn();

作用域、嵌套函数和闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}

const baz = foo(); // 指向函数bar
let ret = baz(); // 20
ret = baz(); // 40
debugger
var blat = foo(); // balt指向另一个bar
ret = blat(); // 20
debugger

上述代码中,返回的对 bar 函数的引用被赋值给变量 baz,bar 函数虽然是在 foo 函数外部,但是依然能够访问变量 a,这是因为 js 中的作用域是词法的。函数是运行在定义它们的作用域中(foo 内部的作用域),而不是运行在调用它们的作用域中。只要 bar 被定义在 foo 中,它就能访问在 foo 中定义的所有变量,即使 foo 的执行已经结束。这是一个闭包的例子,在 foo 返回之后它的作用域就被保存下来,但是只有它返回的那个函数能够访问这个作用域。上面的例子中,baz 和 blat 各有这个作用域及a的一个副本,而且只有它们自己能够对其进行修改。返回一个内嵌函数是创建闭包最常用的手段

通过闭包,我们可以实现私有变量:

1
2
3
4
5
6
7
8
9
10
11
12
function Book(newISBN){
var isbn;
this.setISBN = function(newISBN) {
isbn = newISBN;
}
this.getISBN = function(){
return isbn;
}
// constructor code
this.setISBN(newISBN);
}
Book.prototype.commonMethod = function(){}

上面代码中私有变量通过闭包进行封装可以实现防止无意中修改了变量中的属性。用这种方式创建对象有 2 个弊端:

  1. 新的对象的私有属性和方法都需要占用额外的内存(一般的创建对象所有的方法都存储在原型对象中,内存中只存储一份)
  2. 不利于派生子类,不能访问父类的私有属性和方法

ES6 提供的 Reflect 可以实现真正的属性私有化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function priavateProp(obj, filter){
const handler = {
get(obj, prop) {
if(!filter(prop)){
let val = Reflect.get(obj, prop)
if(typeof val === 'function'){
val = val.bind(obj)
}
return val
}
},
set(obj, prop, val) {
if(filter(prop)){
throw new Error(`cannot set property ${prop}`)
}
return Reflect.set(obj, prop, val)
},
has(obj, prop) {
return filter(prop) ? false : Reflect.has(obj, prop)
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter( prop => !filter(prop))
}
}

return new Proxy(obj, handler)
}

// 私有属性过滤器
// 规则:以 _ 为开头的属性都是私有属性
function filter(prop){
return prop.indexOf('_') === 0
}

const o = {
_private: 'private property',
name: 'public name',
say(){
// 内部访问私有属性
console.log(this._private)
}
}

const p = priavateProp(o, filter)

console.log(p) // Proxy {_private: "private property", name: "public name"}

p._private // undefined
JSON.stringify(p) // "{"name":"public name"}"

// 只能内部访问私有属性
p.say() // private property

console.log('_private' in p) // false

// 不能遍历到私有属性
Object.keys(p) // ["name", "say"]

// 私有属性不能赋值
p._private = '000' // Uncaught Error: cannot set property _private

使用闭包实现 AOP

在 js 中实现 AOP 通常是将一个函数“动态织入”到另一个函数中,具体的实现技术可以通过拓展 Function.prototype 来做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Function.prototype.before = function (beforeFn) {
const self = this;
return function () {
beforeFn.apply(this, arguments);
return self.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) {
const self = this;
return function () {
const ret = self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function doWork() {
console.log('do work');
}

func = doWork
.before(function () {
console.log('before fn');
})
.after(function () {
console.log('after fn');
});

func();
// before fn
// do work
// after fn

以上的代码中,beforeafter 方法都返回一个函数(对原函数进行加工之后的新函数), 在实际调用的时候 beforeafter 的调用顺序是可以互换的;真正的函数也不存在 2 次调用,因为新函数都是对原函数进行了包装。

以先进行 before 调用为例,返回的新函数(假设 fnBefore)就是打印 before fn + 执行 doWork,在进行 after 调用的时候,返回的新函数就是 fnBefore + 打印 after fn。即每次调用都是新包装了一层,不存在多次调用的问题。

使用 AOP 的方式来给函数添加职责,也是 js 语言中一种非常特别和巧妙的装饰者模式实现,这种装饰模式在实际的开发中非常有用!

继承

克隆函数和原型式继承

1
2
3
4
5
6
// Object.create(obj) 的简化版本
const clone = obj => {
function F(){}; // 1. 创建一个空构造函数
F.prototype = obj; // 2. 将其原型指向传入的对象
return new F(); // 3. 返回一个空对象,但原型链指向 `obj`
}

返回的对象没有属性,但是可以通过原型链访问 obj 的方法和属性。

类似于 Object.create 的早期实现(ES5 之前)。它的核心思想是:基于现有对象创建新对象,并可以扩展或修改其属性和方法

原型链继承比类式继承更能节省内存。原型链查找的方式创建的对象使得所有克隆出来的对象共享每个属性和方法的唯一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法的时候,情况才会有所变化。与此同时,在类式继承中创建的每一个对象在内存中都有自己的一份属性(和私有方法)的副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Person = {
init(name, age) {
this.name = name;
this.age = age;
},
show() {
return `name = ${this.name},age = ${this.age}`;
}
};

const s = clone(Person); // 创建Person对象的一个副本,然后改变其中的一些方法
s.showAge = function () {
return this.age;
};
s.init('张三', 14);
console.log(s.show()); // name = 张三,age = 14
console.log(s.showAge()); // 14
  • s 的原型是 Person,所以可以调用 init 和 show
  • 可以动态添加新方法(如 showAge),而不会影响 Person

用这种方法创建的对象往往有较高的内存使用效率,因为它们会共享那些未被改写的属性和方法。那些包含数组和或者对象类型的成员的克隆会有一些麻烦的地方。但是这个问题可以通过一个方法来解决。

多继承与 mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Util = {
show() {
const output = [];
for (const key of Object.keys(this)) {
output.push(`${key} : ${this[key]}`);
}
return output.join(',');
}
}

class Student {
constructor(name, age, courses) {
this.name = name;
this.age = age;
this.courses = courses;
}
}

// 注意:givingClass为带有方法的普通对象
function mixin(receivingClass, givingClass) {
for (const methodName in givingClass) {
if (!receivingClass.prototype[methodName]) {
receivingClass.prototype[methodName] = givingClass[methodName];
}
}
}

mixin(Student, Util);

const s = new Student('张三丰', 83, ['太极拳', '乾坤大罗伊']);
const str = s.show();

用一些代码来拓展一个类有时候比继承一个类更加适合,这可以轻松解决类中的重复代码问题。

上面的代码可以将工具函数混入到类的原型中实现多继承,注意混入的类不能是 ES6 的 class 而是一个普通对象。因为 ES6 中定义的实例方法是不可枚举的。

单例

惰性单例:在需要的时候才创建对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var getSingle = function(fn){
var result;
return function(){
return result || (result = fn.apply(this, arguments));
}
}

var createSingle = getSingle(function(){
return new Object('single');
})

var single1 = createSingle();
var single2 = createSingle();
console.log(single1 === single2); // true

分支

一种用来把浏览器差异封装到运行期间内动态进行设置的技术。例如 XHR 对象在早期的 IE 中是 ActiveX 对象。如果不采用这种技术,每次调用这个方法的时候浏览器嗅探代码会再次运行,严重缺乏效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const XHRFactory = (function () {
const standard = {
createXHR() {
return new XMLHttpRequest();
}
};
const activeXNew = {
createXHR() {
return new ActiveXObject('Msxml2.XMLHTTP');
}
};
const activeXOld = {
createXHR() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
};

let testObj;
try {
testObj = standard.createXHR();
return standard;
} catch (e) {
try {
testObj = activeXNew.createXHR();
return activeXNew;
} catch (error) {
try {
testObj = activeXOld.createXHR();
return activeXOld;
} catch (error) {
throw new Error('no xhr object found in this enviroment');
}
}
}
})();

上面的代码之所以有效的原因是:在脚本加载时一次性确定特定浏览器的代码。这样一来初始化完成之后每种浏览器只会执行针对它的 js 代码。在使用分之的时候需要在内存(分支计数需要多创建对象)和时间之间做一个权衡。

策略模式

定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对 Context 发起请求的时候,Context 总是把请求委托给这些策略对象中间的某一个进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const strategies = {
add(a, b) {
return a + b;
},
sub(a, b) {
return a - b;
},
mul(a, b) {
return a * b;
}
}
const calc = function (a, b, method) {
return strategies[method](a, b);
}
console.log(calc(1, 2, 'add'));
console.log(calc(1, 2, 'sub'));
console.log(calc(1, 2, 'mul'));

使用策略模式可以消除原程序中大片的条件分支。所有和计算相关的逻辑不再放在 Context 中,而是分布在各个策略对象中。Context 本身并没有计算的能力,而是把这个职责委托给了各个策略对象。每个策略对象负责的算法被封装在对象内部。当我们对这些策略对象发出“计算”请求的时候他们会返回不同的结果,这正是对象多态性的体现,也是它们可以相互替换的目的。替换 Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

使用策略模式完成表单校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const strategies = {
notEmpty(value, errMsg) {
if (value === "") {
return errMsg;
}
},
minLength(value, length, errMsg) {
if (value.length < length) {
return errMsg;
}
},
isMobile(value, errMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errMsg;
}
},
};

class Validator {
constructor() {
this.cache = []; // 保存检验规则
}
add(dom, rule, errMsg) {
const ary = rule.split(":");
// 把校验的步骤用空函数包装起来并且放入 cache
this.cache.push(() => {
const straegy = ary.shift();
ary.unshift(dom.value);
ary.push(errMsg);
return strategies[straegy].apply(dom, ary);
});
}
valid() {
for (const validFn of this.cache) {
const msg = validFn();
if (msg) {
return msg;
}
}
}
}

function validate() {
const regForm = document.querySelector("#reg");
regForm.onsubmit = () => {
const validator = new Validator();

validator.add(regForm.username, "notEmpty", "用户名不能为空");
validate.add(regForm.password, "minLength:6", "密码长度不能小于 6 位");
validator.add(regForm, phone, "isMobile", "手机号格式不正确");

const errMsg = validator.valid();
if (errMsg) {
// 未通过校验则阻止表单提交
alert(errMsg);
return false;
}
};
}

我们可以仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式方便一致到其他项目中。

享元

运用共享技术来有效支持大量细粒度对象。

工厂模式

简单工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Toyota {
constructor() {
this.name = '丰田';
}
}
class Audi {
constructor() {
this.name = '奥迪';
}
}
class BMW {
constructor() {
this.name = '宝马';
}
}

const inters = ['wash', 'run', 'repair'];
for (const clazz of [Toyota, Audi, BMW]) {
for (const inter of inters) {
clazz.prototype[inter] = function () {
console.log(`${this.name} -> ${inter}`);
}
}
}

// 汽车接口,包含洗车、运行、维修接口
const Car = new Intercafe('Car', inters);

class CarFactory {
static makeCar(model) {
let car;
switch (model) {
case 'toyota':
car = new Toyota();
break;
case 'audi':
car = new Audi();
break;
case 'bmw':
car = new BMW();
break;
}
Intercafe.ensureImplements(car, Car); // 确保对象实现了汽车接口
return car;
}
}
class CarShop {
static sellCar(model) {
const car = CarFactory.makeCar(model);
car.wash();
return car;
}
}

上面的例子中将创建对象的工作转交给一个外部对象,因为创建对象的方式可能存在变化。

接口在工厂模式中起着很重要的作用。如果不对对象进行类型检查以确保实现了必须的方法,工厂模式所带来的所处也所剩无几。创建对象并且对它们一视同仁。

工厂方法

工厂方法和简单工厂的区别在于不是使用另外一个来来创建对象,而是使用一个子类。工厂是将一个成员对象推迟到子类中进行的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
const AjaxHandler = new Intercafe('AjaxHandler', ['request', 'createXhr']);

class SimpleHandler {
constructor() {
this.createXhr = null;
}
request(method, url, callback, postVars) {
const xhr = this.createXhr();
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
xhr.status === 200 ? callback.success(xhr.responseText, xhr.responseXML) : callback.failure(xhr.status);
}
xhr.open(method, url, true);
if (method !== 'POST') postVars = null;
xhr.send(postVars);
}
createXhr() {
const methods = [
function () { return new XMLHttpRequest(); },
function () { return new ActiveXObject('Msxml2.XMLHTTP'); },
function () { return new ActiveXObject('Microsoft.XMLHTTP'); }
];
for (const method of methods) {
try {
method();
} catch (e) {
continue;
}
this.createXhr = method; // memoize the method
return method;
}
throw new Error('SimpleHandler:Can not create an xhr object.');
}
}

class QueueHandler extends SimpleHandler {
constructor() {
this.queue = [];
this.requestInProcess = false;
this.retryDelay = 5;
}
advanceQueue() {
if (this.queue.length === 0) {
this.requestInProcess = false;
return;
}
const req = this.queue.shift();
this.request(req.method, req.url, req.callback, req.postVars, true);
}
request(method, url, callback, postVars, override) {
if (this.requestInProcess && !override) {
this.queue.push({ method, url, callback, postVar });
} else {
this.requestInProcess = true;
const xhr = this.createXhr();
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
callback.success(xhr.responseText, xhr.responseXML)
this.advanceQueue();
} else {
callback.failure(xhr.status);
setTimeout(() => {
this.request(method, url, callback, postVars, true);
}, this.retryDelay * 1000);
}
}
xhr.open(method, url, true);
if (method !== 'POST') postVars = null;
xhr.send(postVars);
}
}
}

class OfflineHandler extends SimpleHandler {
constructor() {
this.storedRequests = [];
}
request(method, url, callback, postVars) {
if (XhrManager.isOffline()) {
this.storedRequests.push({ method, url, callback, postVar });
} else {
// online的时候使用父类发送xhr
this.flushStoredRequests();
super.request(method, url, callback, postVars);
}
}
flushStoredRequests() {
for (const req of this.storedRequests) {
super.request(req.method, req.url, req.callback, req.postVars);
}
}
}

const XhrManager = {
createXhrHandler() {
let xhr;
if (this.isOffline()) {
xhr = new OfflineHandler();
} else if (this.isHighLatency()) {
xhr = new QueueHandler();
} else {
xhr = new SimpleHandler();
}
Intercafe.ensureTmplements(xhr, AjaxHandler);
return xhr;
},
isOffline() { },
isHighLatency() { }
};

上述 createXhr 中 memoize 技术,复杂的设置代码只会在首次调用的时候执行一次,此后就只有针对当前浏览器的代码会被执行。

上面的工厂方法模式可以根据网络条件创建专门的请求对象:OfflineHandler 会在用户离线的时候把请求缓存起来,而 QueueHandler 会在发起新的请求之前确保所有请求都已经成功处理,如果请求失败后还支持指定时间间隔后重试这个请求,直到成功为止。

现在我们只需要使用XhrManager.createXhrHandler这个工厂方法而不需要实例化特定的类了。

工厂方法模式的好处在于消除对象间的耦合,通过工厂方法而不是 new 关键字可以把所有的实例化代码集中在一个位置,从而可以大大简化更换所用的类或者在运行期间选择类的工作。在派生子类的时候也提供了更大的灵活性 ———— 先创建一个抽象的父类,在子类中创建工厂方法,从而把成员对象的实例化推迟到专门的子类中进行。

模板方法

由 2 部分组成:抽象父类 + 具体实现的子类。通常在抽象父类中封装了子类的算法框架,包括一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class Beverage {  // 饮料抽象类
final void init() { // 模板方法
boilWater();
brew();
pourInCup();
addCondiments();
}

void boilWater() { // 具体方法 boilWater
System.out.println("把水煮沸");
}

abstract void brew(); // 抽象方法 brew
abstract void addCondiments(); // 抽象方法 addCondiments
abstract void pourInCup(); // 抽象方法 pourInCup
}

public class Tea extends Beverage {
@Override
void brew() {
System.out.println("用沸水浸泡茶叶");
}

@Override
void pourInCup() {
System.out.println("把茶倒进杯子");
}

@Override
void addCondiments() {
System.out.println("加柠檬");
}
}

public class Coffee extends Beverage {
@Override
void brew() {
System.out.println("用沸水冲泡咖啡");
}

@Override
void pourInCup() {
System.out.println("把咖啡倒进杯子");
}

@Override
void addCondiments() {
System.out.println("加糖和牛奶");
}
}

从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了项目的骨架,程序员在继承了框架的结构之后,负责往里面填空。

模板方法模式是好莱坞原则(别调用我们,我们会调用你,高层组件调用低层组件)的一个典型应用。当使用模板方法模式的时候意味着子类放弃了对自己的控制权,而是改用父类去通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。除此之外好莱坞原则还常常用于发布-订阅模式和回调函数。

对 js 设计模式的误解

在 js 中第一个问题是习惯性将静态语言的设计模式照搬到 js 中,例如有人为了在 js 中模仿 Factory Method 模式,而生硬地将创建对象的步骤延迟到子类中,而实际上,在 Java 等静态语言中,让子类来“决定”创建何种对象的原因是为了让程序迎合 DIP 原则。在这些语言中创建对象的时候哦,先解开对象类型之间的耦合关系非常重要,这样才有机会在将来让对象表现出多态性。而在 js 这种类型模糊的语言中,对象多态是天生的,一个变量既可以指向一个类,又可以随时执行另一个类 —— js 中并不存在类型耦合的问题,自然也没有必要刻意去把对象的创建“推迟”到子类中,也就是说 js 实际上是并不需要工厂方法模式的。模式的存在首先是能为我们解决什么问题,这种牵强的模拟只会让人觉得设计模式既无用又麻烦!

另一个问题是习惯根据模式的名字去臆测模式的一切。例如命令模式的本意是把请求封装到对象中,利用命令模式可以解开请求发送者和请求接受者之间的耦合。但是命令模式经常被误解为只有一个名为 execute 的普通方法调用。这个方法除了叫做 execute 之外,其实并没有看出其他用途。所以很多人会误会命令模式的意图,以为它其实没有什么用途,从而联想到其他设计模式也没有什么用途。