软件设计最大的难题就是应对需求的变化,但是纷繁复杂的需求变化又是不可预料的,我们要为不可预料的变化做好准备,这本身是一件非常痛苦的事情,但好在有大师们已经给我们提出了非常好的六大设计原则和 23 种设计模式来“封装”未来的变化。

在软件设计上有一些经典的设计原则,其中包括,SOLID、KISS、YAGNI、DRY、LOD 等。这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但是用在项目中的时候就会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。我有时会对这些原则理解的不够透彻,导致在使用时过于教条注意,拿原则当真理,生搬硬套,适得其反。

下面我对这些原则做一些梳理和整理,确保自己理解的是正确的。

SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:

  • 单一职责原则
  • 开闭原则
  • 里氏替换原则
  • 接口隔离原则
  • 依赖反转原则

分别对应 SOLID 的 S、O、L、I、D 这 5 个英文字母。

原则描述

单一职责原则和开闭原则的远离比较简单,但是想要在实践中用却比较难。但依赖反转原则却正好相反,这个原则用起来比较简单,但概念起来比较难。

依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则。

1
高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计。

依赖反转的就是指高层模块和底层模块之间的依赖关系被反转了,高层低层模块都应该依赖抽象接口。

控制反转和依赖注入这两个概念和依赖反转是有区别的。

控制反转是一个比较笼统的设计,强调将程序的执行流程由框架来控制,预留扩展点给程序员填充业务逻辑。

而依赖注入是具体的编码技巧,将一个类所依赖的类通过构造函数的方式传入,而不是在构造函数里面硬编码确定所依赖类的类型。而且所依赖类的类型应该是一个接口,存在多种实现,这样就可以提高了代码的扩展性,可以灵活地替换依赖的类。

需要了解依赖反转原则的话,需要弄清楚以下问题:

  • “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?
  • 业界还存在“控制反转”和“依赖注入”这两个概念,这两个概念跟“依赖反转”有什么区别和联系,它们说的是同一件事情吗?

控制反转

在讲“依赖反转原则”之前,先来看看“控制反转”。控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。

这里的控制指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在业界有一些框架是可以实现反转的,也就是整个程序的执行流程可以通过框架来控制,流程的控制权从程序员“反转”到了框架。

实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有依赖注入。所以这里的控制反其实并不是一种具体的实现技巧,而是一种比较笼统的设计思想,一般用来知道框架层面的设计。

依赖注入

依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。

那到底什么是依赖注入呢?

1
不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或者叫注入)给类使用。

用代码来解释一下:

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

// 非依赖注入实现方式
public class Notification {
private MessageSender messageSender;

public Notification() {
this.messageSender = new MessageSender(); //此处有点像hardcode
}

public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}

public class MessageSender {
public void send(String cellphone, String message) {
//....
}
}
// 使用Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
private MessageSender messageSender;

// 通过构造函数将messageSender传递进来
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}

public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。

这一点在“开闭原则”的时候也提到过。当然,上面代码还有继续优化的空间,我们还可以把 MessageSender 定义成接口,基于接口而非实现编程。改造后的代码如下所示:

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

public class Notification {
private MessageSender messageSender;

public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}

public void sendMessage(String cellphone, String message) {
this.messageSender.send(cellphone, message);
}
}

public interface MessageSender {
void send(String cellphone, String message);
}

// 短信发送类
public class SmsSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}

// 站内信发送类
public class InboxSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}

//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);