Pro JavaScript Design Patterns 笔记.part.2

笔记的存在,用来下一次记忆知识点时,可以快速地找到内容,而不至于再去翻整本书;以及加深记忆(避免看了等于白看)。

这些都是在书上画线标记了的。
这些都是在书上画线标记了的。
这些都是在书上画线标记了的。

重要的事情要说三遍。

此外注意:此系列中出现的术语都是《JavaScript高级程序设计》中出现的术语,例如此书中出现的一些不符的名词(例如将类型、甚至对象称为类)都将被更准确的类型、对象代替。

第二章 接口

本章具体说明接口的重要性,以及确保类型是否实现了相应接口的手段(比较鸡肋)。

在正式内容之前,想起两件事情(我是分割线,代表可看可不看):


事一:某次业务需求过程中需要一个弹窗,这个弹窗的作用就是点击某个id元素,将对应的标题、二维码等显示在弹窗中。因时间关系,思考了所需要的对象的方法,类似:

1
2
3
4
5
dialog
// 参数为 id title qrcode
// 内部判断是否初始化(弹窗html的处理 插入到页面里)、以及id是否和上一次显示的一致(一致则直接显示)
.show
.hide

但是最后得到的却是这样的:
在获得dialog之前,需要调用初始化函数A并传入所有参数options:A({id: 1, title: '1', qrcode: 'xxx'})。A内部做的操作,是将这几个参数写死到相应的弹窗dom结构中,后续不能改变。最后返回dialog对象,show方法不包含任何参数。这个结果也就导致最后上线前花了两小时临时(非常粗暴地)修正了这个写法,并附上临时fix的标志。

事二:PC前端与后端约定了ajax接口,H5前端也共用此接口。然后后端把接口返回的某一个参数改掉了,忘了和H5端的同学说,使得某一个业务场景,H5端同学调试花了不少时间。

这些也统称为接口,就重要性来说,不言而喻。包括前端不用等待后端开发,前端与前端之间的协同开发等等。


接口之利:

  • 促进代码重用(让人知道有这样的东西实现了这样的功能)
  • 有助于稳定不同类型之间的通信(也就是方便开发)
  • 错误被限制在极小部分代码中,查找修复起来也很方便

接口之弊:

  • JavaScript没有Interface、implements关键字

对应的,有几种模拟Interface、implements这些关键字定义接口的方法。(其实比较扯)

一、用注释描述接口

1
2
3
4
5
6
7
8
9
10
11
/*
interface Composite {
function add(child);
function remove(child);
function getChild(index);
}

interface FormItem {
function save();
}
*/

缺点:没有为是否真正实现了正确的方法集而进行检查,也不会抛出错误,对测试和调试没有什么帮助,主要还是属于程序文档的范畴。(对于小项目来说,我倒是觉得一个接口文档已经非常足够了,接口没有实现的问题,是程序员的问题)。

二、用属性检查模仿接口

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
var CompositeForm = function (id, method, action) {
// 宣称实现了Composite FormItem 这两个接口
this.implementsInterfaces = ['Composite', 'FormItem'];
};

function addForm (formInstance) {
// 调用时的属性检查
if (!implements(formInstance, 'Composite', 'FormItem')) {
throw new Error('Object does not implement a required interface');
}
// do something...
}

// 实际其作用就是两个循环比对接口名称和定义的名称是否一致
// 也就验证其是否含有定义的属性
function implements(object) {
var i = 1,
j = 0,
interfaceName = '',
interfaceFound = false;

for ( ; i < arguments.length; i += 1) {
interfaceName = arguments[i];
interfaceFound = false;

for (j = 0; j < object.implementsInterfaces.length; j += 1) {
if (object.implementsInterfaces[j] == interfaceName) {
interfaceFound = true;
break;
}
}

if (!interfaceFound) {
return false;
}
}

return true;
}

缺点:并未确保构造的类型真正实现了自称实现的接口(只是拥有指定的属性)。
个人观点:实际上来说,接口的检查,只要通过编辑器几个折叠就能看出是否实现了对应的方法等等。并没有太大的必要和意义做这些检查,除非是成百上千的方法集。

三、用鸭式辨型模仿接口

鸭式辨型:像鸭子一样走路并且嘎嘎叫的就是鸭子。它把对象实现的方法集作为判断它是不是某个类型的实例的唯一标准(倒是比属性检查的方法好一点,毕竟是没有侵入对象内部给其添加属性的,这对于外部来源的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
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
// Interface类型 用于创建Interface实例 包含name methods属性
var Interface = function (name, methods) {
var i = 0,
len = arguments.length;

if (arguments.length !== 2) {
throw new Error('Interface constructor called with ' + arguments.length +
'arguments, but expected exactly 2.');
}

this.name = name;
this.methods = [];

for (; i < len; i += 1) {
if (typeof methods[i] !== 'string') {
throw new Error ('Interface constructor expects method names to be passed as a string');
}
}

this.methods.push(methods[i]);
};

// Interface类型上的静态方法
// 第一个参数为需要检测的对象,后面的参数是Interface的实例或实例们
// 此方法的作用,也只是循环Interface的实例,循环实例上的methods方法,判断需要检测的对象是否包含这些方法
Interface.ensureImplements = function (object) {
var i = 1,
j = 0,
methodsLen = 0,
method = '',
interface = '',
len = arguments.length;

if (len < 2) {
throw new Error ('Function Interface.ensureImplements called with ' +
len + 'arguments, but expected at least 2.');
}

for ( ; i < len; i += 1) {

interface = arguments[i];

if (interface.constructor !== Interface) {
throw new Error('Function Interface.ensureImplements expects arguments' +
'two and above to be instances of Interface');
}

for (methodsLen = interface.methods.length; j < methodsLen; j += 1) {
method = interface.methods[j];

if (!object[method] || typeof object[method] !== 'function') {
throw new Error('Function Interface.ensureImplements:object ' +
'does not implement the ' + interface.name +
' interface.Method ' + method + 'was not found .');
}
}
}
};

// 接口校验 -- 调用
// 生成两个Interface的实例对象 包含name和methods属性
var compositeInt = new Interface('Composite', ['add', 'remove', 'getChild']);
var formInt = new Interface('FromItem', ['save']);

// CompositeForm 需要符合 上面两个接口
var CompositeForm = function () {};
var cfInstance = new CompositeForm();

// 实际校验 是否符合接口
// 循环 compositeInt 和 formInt 这两个Interface 实例对象的methods属性
// 通过判断最后实际应用的cfInstance 实例对象是否包含接口所定义的所有方法而不是判断其是哪个构造函数的实例
var functionCall = function (cfInstance) {
Interface.ensureImplements(cfInstance, compositeInt, formInt);
cfInstance.add();
cfInstance.getChild();
cfInstance.save();
cfInstance.remove();
};

谨慎地使用Interface类型有助于创造更健壮的类型和更稳定的代码。但是对于小型的、不太费事的项目来说,接口的好处并不明显,只是徒增复杂度而已,因此还需权衡利弊,判断在代码中使用接口是否划算。

本书采用的接口实现方式:第一种和第三种的结合使用。

依赖于接口的设计模式:

  • 工厂模式
  • 组合模式
  • 装饰者模式
  • 命令模式