# 面向切面的编程思想

# SOLID ⾯面向对象设计原则之一

# 原则基本概念

程序设计领域, SOLID (单⼀一功能、开闭原则、里氏替换、接⼝口隔离以及依赖反转)是由罗伯特·C·⻢马丁在21世纪早期引入记忆术⾸首字⺟母缩略略字,指代了了⾯面向对象编程和⾯面向对象设计的五个基本原则。当这些原则被⼀一起应⽤用时,它们使得⼀一个程序员开发⼀一个容易易进⾏行行软件维护和扩展的系统变得更更加可能SOLID被典型的应⽤用在测试驱动开发上,并且是敏敏捷开发以及⾃自适应软件开发的基本原则的重要组成部分。

# 这⼏个字母的代表含义

# [S] 单⼀功能原则

单⼀功能原则 :单⼀功能原则认为对象应该仅具有⼀种单⼀功能的概念。 换句话说就是让一个类只做⼀种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。在所有的SOLID原则中,这是⼤多数开发⼈员感到能完全理解的⼀条。严格来说,这也可能是违反频繁的⼀条原则了了。单⼀责任原则可以看作是低耦合、⾼内聚在面向对象原则上的引申,将责任定义为引起变化的原因,以提⾼内聚性来减少引起变化的原因。责任过多,可能引起它变化的原因就越多,这将导致责任依赖,相互之间就产⽣影响,从而极⼤的损伤其内聚性和耦合度。单⼀责任,通常意味着单⼀的功能,因此不要为⼀个模块实现过多的功能点,以保证实体只有⼀个引起它变化的原因。

demo

//Bad
class UserSettings {
    public user
    constructor(user) {
        this.user = user;
    }

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

    verifyCredentials(): boolean {
        // ...
        return false;
    }
}
//Good:
class UserAuth {
    public user
    constructor(user) {
        this.user = user;
    }
    verifyCredentials() {
        // ...
    }
}

class UserSetting {
    public user;
    public auth
    constructor(user) {
        this.user = user;
        this.auth = new UserAuth(this.user);
    }
    changeSettings(settings) {
        if (this.auth.verifyCredentials()) {
            // ...
        }
    }
}
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

# [O] 开闭原则

开闭原则(ocp) 认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。 软件实体应该是可扩展,⽽而不不可修改的。也就是说,对扩展是开放的,⽽而对修改是封闭的(“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不不应该被修改。也就是说你可以新增功能但不不能去修改源码)。这个原则是诸多⾯面向对象编程原则中抽象、难理解的⼀个。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类⼀旦设计完成,就可以独⽴完成其工作,⽽不要对类进行任何修改。可以使⽤变化和不变来说明:封装不变部分,开放变化部分,⼀般使用接⼝继承实现⽅式来实现“开放”应对变化,说⼤⽩话就是:你不是要变化吗?,那么我就让你继承实现⼀个对象,⽤一个接口来抽象你的职责,你变化越多,继承实现的⼦类就越多。

demo

//Bad:
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:
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
    });
  }
}

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

# [L] 里氏替换原则

里氏替换原则 :里氏替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的⼦类所替换的”的概念。子类必须能够替换成它们的基类。即:子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。另外,不应该在代码中出现if/else之类对⼦类型进行判断的条件。里氏替换原则LSP是使代码符合开闭原则的⼀个重要保证。正是由于⼦类型的可替换性才使得父类型的模块在⽆需修改的情况下就可以扩展。在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们⼀个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。继承对于OCP,就相当于多态性对于里氏替换原则。子类可以代替基类,客户使用基类,他们不需要知道派生类所做的事情。这是一个针对行为职责可替代的原则,如果S是T的子类型,那么S对象就应该在不改变任何抽象属性情况下替换所有T对象。

//Bad:
// 长方形
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
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);

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

# [I] 接⼝隔离原则

接⼝隔离原则 :接⼝隔离原则 认为“多个特定客户端接⼝要好于⼀个宽泛⽤途的接⼝”的概念。 不能强迫⽤户去依赖那些他们不使⽤的接口。换句话说,使⽤多个专门的接口⽐使⽤单一的总接口总要好(JavaScript 几乎没有接口的概念所以使用ts)。注意:在代码中应⽤用ISP并不一定意味着服务就是绝对安全的。仍然需要采⽤良好的编码实践,以确保正确的验证与授权。这个原则起源于施乐公司,他们需要建⽴了一个新的打印机系统,可以执行诸如装订的印刷品⼀套,传真多种任务。此系统软件创建从底层开始编制,并实现了这些任务功能,但是不断增⻓的软件功能却使软件本身越来越难适应变化和维护。每⼀次改变,即使是⼩小的变化,有人可能需要近一个小时的重新编译和重新部署。这是几乎不可能再继续发展,所以他们聘请罗伯特Robert帮助他们。他们首先设计了一个主要类Job,⼏乎能够用于实现所有任务功能。只要调⽤Job类的一个⽅法 就可以实现⼀个功能,Job类就变动非常⼤,是一个胖模型啊,对于客户端如果只需要⼀个打印功能,但是其他⽆关打印的⽅法功能也和其耦合,ISP 原则建议在客户端和Job类之间增加⼀个接⼝层,对于不同功能有不同接⼝,⽐如打印功能就是Print接⼝,然后将⼤的Job类切分为继承不同接口的子类,这样有⼀个Print Job类,等等。

demo

//Bad:
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:
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
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

# [D] 依赖反转原则

*依赖倒置原则(Dependency Inversion Principle,DIP)*规定:代码应当取决于抽象概念,⽽不是具体实现。*⾼层模块不应该依赖于低层模块,⼆者都应该依赖于抽象 抽象不应该依赖于细节,细节应该依赖于抽象 (总结解耦)*类可能依赖于其他类来执行其工作。但是,它们不应当依赖于该类的特定具体实现,而应当是它的抽象。这个原则实在是太重要了,社会的分⼯工化,标准化都是这个设计原则的体现。显然,这⼀概念会⼤大提高系统的灵活性。如果类只关心它们用于支持特定契约而不是特定类型的组件,就可以快速而轻松地修改这些低级 服务的功能,同时大限度地降低对系统其余部分的影响。

demo

//Bad
// 库存查询
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:
// 库存跟踪
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();

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

# 控制反转(Inversion of Control) IOC

# 什什么是控制反转

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以⽤来减低计算机代码之间的耦合度。其中常⻅的⽅式叫做依赖注入(Dependency Injection,简称DI),还有⼀种⽅式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由⼀个调控系统内所有对象的外界实体,将其所依赖的对象的引⽤传递给它。也可以说,依赖被注入到对象中。

依赖查找: 容器提供回调接口和上下文条件给组件 依赖注入: 组件不做定位查询,只提供普通的方法让容器去决定依赖关系。

# AOP 面向切⾯(Aspect Oriented Programming)

在软件业,AOP为Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的⼀种技术。AOP是OOP的延续,是软件开发中的⼀个热点,也是Spring框架中的⼀个重要内容,是函数式编程的⼀种衍⽣范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

# 基础概念

AOP完善spring的依赖注⼊(DI)⾯向对象编程将程序分解成各个层次的对象,面向切面编程将程序运行过程分解成各个切面。

# Filter

Filter(过滤器)也是一种AOPA 它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重⽤用模块,并将其命名为 "Aspect",即切面。所谓"切面"。

# 优点

AOP的好处就是你只需要⼲你的正事,其它事情别人帮你干。在你访问数据库之前,自动帮你开启事务,当你访问数据库结束之后,⾃动帮你提交/回滚事务!