SOLID

SOLID 是几个单词首字母组合而来,分别表示 单一功能原则开闭原则里氏替换原则接口隔离原则以及依赖反转原则

单一功能原则

如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserSettings {
constructor(user) {
this.user = user;
}

changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}

verifyCredentials() {
// ...
}
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}

class UserSetting {
constructor(user) {
this.user = user;
this.auth = new UserAuth(this.user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}

开闭原则

“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。

Bad:

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
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
}

class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
}

class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}

fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// 传递 response 并 return
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// 传递 response 并 return
});
}
}
}

function makeAjaxCall(url) {
// 处理 request 并 return promise
}

function makeHttpCall(url) {
// 处理 request 并 return promise
}

Good:

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
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}

request(url) {
// 处理 request 并 return promise
}
}

class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}

request(url) {
// 处理 request 并 return promise
}
}

class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}

fetch(url) {
return this.adapter.request(url).then((response) => {
// 传递 response 并 return
});
}
}

里氏替换原则

名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。

Bad:

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
// 长方形
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}

setColor(color) {
// ...
}

render(area) {
// ...
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

// 正方形
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}

setHeight(height) {
this.width = height;
this.height = height;
}
}

function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea();
rectangle.render(area);
});
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Good:

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
class Shape {
setColor(color) {
// ...
}

render(area) {
// ...
}
}

class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Shape {
constructor(length) {
super();
this.length = length;
}

getArea() {
return this.length * this.length;
}
}

function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

接口隔离原则

JavaScript 几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}

traverse() {
// ...
}
}

const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});

Good:

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
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}

setupOptions() {
if (this.options.animationModule) {
// ...
}
}

traverse() {
// ...
}
}

const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule() {}
}
});

依赖反转原则

说就两点:

  1. 高层次模块不能依赖低层次模块,它们依赖于抽象接口。
  2. 抽象接口不能依赖具体实现,具体实现依赖抽象接口。

总结下来就两个字,解耦。

Bad:

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
// 库存查询
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

// 库存跟踪
class InventoryTracker {
constructor(items) {
this.items = items;

// 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。
this.requester = new InventoryRequester();
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

Good:

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
// 库存跟踪
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

// HTTP 请求
class InventoryRequesterHTTP {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

// webSocket 请求
class InventoryRequesterWS {
constructor() {
this.REQ_METHODS = ['WS'];
}

requestItem(item) {
// ...
}
}

// 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 webSocket 请求。
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP());
inventoryTracker.requestItems();