心游于艺,道不远人

变继承关系为组合关系

State模式

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

State设计模式

装饰器模式

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

继承

类式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
// 注意:这里不能用箭头函数
return this.name;
};

function Author(name, books) {
Person.call(this, name); // 使用this上下文调用父类的构造器
this.books = books;
}
// 子类原型指向父类原型
Author.prototype = new Person();
Author.prototype.constructor = Author; // constructor被改变成了Person,需要再次修正回来
Author.prototype.getBooks = function () { // 添加子类特有方法
return this.books;
}

抽象一个函数用于继承:

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
function extend(subClass, superClass) {
// 改进:添加一个空函数并将其创建的对象实例插入到原型链中,因为超类的实例可能比较大或者包含比较多的运算
const F = function () { };
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;

// 改进引用父类的方式,从而子类中可以使用 `子类名.superClass_`得到父类的名称,比起Person.call(this, name)更具有通用性,同时有了superClass_也可以直接调用超类中的方法
subClass.superClass_ = superClass.prototype;
if (superClass.prototype.constructor === Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}

function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};

function Author(name, books) {
this.books = books;
Author.superClass_.constructor.call(this, name);
}
extend(Author, Person);
Author.prototype.getBooks = function () {
return this.books;
}
Author.prototype.getName = function () {
const name = Author.superClass_.getName.call(this);
return name + '->' + this.getBooks().join(',');
};

const a = new Author('村上春树', ['挪威的森林', '1Q84'])
const info = a.getName();

上面的代码可以使用node中自带的inherits进行简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
// 注意:这里不能用箭头函数
return this.name;
};

function Author(name, books) {
Person.call(this, name); // 调用父类构造器,这一步不能省略
this.books = books;
}
Author.prototype.getBooks = function () {
return this.books;
}

require('util').inherits(Author, Person);

node中实现实际上是通过Object.setPrototypeOf(ctor.prototype, superCtor.prototype)将子类原型设置和父类一样。幸运的是ES6提供了class关键字和extends关键字,再也不用这么麻烦了,ES6大法好!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Author extends Person {
constructor(name, books) {
super(name);
this.books = books;
}
getBooks() {
return this.books;
}
}

克隆函数和原型式继承

1
2
3
4
5
const clone = obj => {
function F(){};
F.prototype = obj;
return new F(); // 返回一个以给定原型为对象的空对象
}

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

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());
console.log(s.showAge());

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

多继承与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中定义的实例方法是不可枚举的。

单例

js中的单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么只能被实例化一次。

对象字面量

1
2
3
4
5
6
7
8
const Singleton = {
attr1: true,
attr2: 10,
_privateAttr1: 'hehe',
method1() { },
method2() { },
_privateMethod(){ }
}

这种方式创建的单体对象在划分命名空间的时候非常有用:代码分模块组织,增强代码的可读性。有了命名空间也可以减少变量的冲突。

上面的代码使用约定的下划线开头表示私有成员,使用闭包可以实现完全私有:

1
2
3
4
5
6
7
8
9
10
11
const Singleton = (function () {
// 私有成员
const privateAttr = 1;
const privateMethod = () => 1;

// 共有成员
return {
publicAttr: true,
publicMethod(args) { }
}
})();

上面的这种模式又称为模块模式(module pattern),指的是把一批相关的属性和方法组织成模块并起到划分命名空间的作用。

上面的这种方式实现私有的优势:私有成员不会在单例对象外部被访问,可以自由改变对象的实现细节,而不会殃及无辜。

惰性实例化

生成对资源密集型或者配置开销非常大的单体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Singleton = (function () {
let uniqInstance;
// 构造器私有化,不能从外面访问
function constructor() {
// 私有成员
const privateAttr = 1;
const privateMethod = () => 1;

// 共有成员
return {
publicAttr: true,
publicMethod(args) { }
}
}
return {
getInstance() {
if (!uniqInstance) uniqInstance = constructor();
return uniqInstance;
}
};
})();

分支

一种用来把浏览器差异封装到运行期间内动态进行设置的技术。例如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代码。

在使用分之的时候需要在内存(分支计数需要多创建对象)和时间之间做一个权衡

方法的链式调用

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
class Student {
constructor() {
this.name = 'defalut';
this.age = 0;
}
setName(name) {
this.name = name;
return this;
}
setAge(age) {
this.age = age;
return this;
}
getName(cb) {
// 直接使用return会终止链式调用,采用回调函数可以解决
// return this.name;
cb.call(this, this.name);
return this;
}
getAge(cb) {
cb(this.age);
return this;
}
}

new Student().getName(console.log)
.setAge(10).getAge(console.log)

要想实现链式调用,每次方法调用的时候返回this即可。这对于赋值器来说没有什么影响,但是对于取值器来说需要得到返回值,为了保持API的一致可以使用回调函数将值返回。

工厂模式

简单工厂

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关键字可以把所有的实例化代码集中在一个位置,从而可以大大简化更换所用的类或者在运行期间选择类的工作。在派生子类的时候也提供了更大的灵活性 ———— 先创建一个抽象的父类,在子类中创建工厂方法,从而把成员对象的实例化推迟到专门的子类中进行。