在程序设计领域, SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
概述
首字母 |
全称 |
中文 |
S |
The Single Responsibility Principle(SRP) |
单一责任原则 |
O |
The Open-Closed Principle(OCP) |
开放-封闭原则 |
L |
The Liskov Substitution Principle(LSP) |
里氏替换原则 |
I |
The Interface Segregation Principle(ISP) |
接口聚合原则 |
D |
The Dependency Inversion Principle(DIP) |
依赖转置原则 |
单一责任原则(SRP)
单一责任原则是指一个类应当只具有一个责任,仅具有一个功能。这样可以使得每个类的内部结构更加简单,更容易维护。
如果一个类的功能过多,会导致代码的耦合度过高,引入额外的包,占据资源以及导致频繁的重新配置、部署等(就像一个人,如果要管的事情太多,就容易出错)。自然容易想到,一个简单的解决方式为将职责划分到多个类中。
下面是单一责任原则的一个反例:
1
2
3
4
5
|
interface MailClient {
void receiveMail();
void classifyMail();
void sendMail();
}
|
上面的MailClient
接口的要求实现的功能就太多,可以适当拆分。
1
2
3
4
5
6
7
8
9
10
11
|
interface Receiver {
void receiveMail();
}
interface Classification {
void classifyMail();
}
interface Sender {
void sendMail();
}
|
SRP 最简单的原则,却是最难做好的原则
开放-封闭原则(OCP)
开放-封闭原则包含两个部分:对拓展性的开放(be open for extension),对修改的封闭(be closed to modification)。
对拓展性的开放意味着模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化;而对修改的封闭意为模块自身的代码不应该被修改,这样可以认为它具有固定的行为。
伯特兰·迈耶一般被认为是最早提出开闭原则这一术语的人,在他 1988 年发行的《面向对象软件构造》中给出:这一想法认为一旦完成,一个类的实现只应该因错误而修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。衍生的子类可以或不可以拥有和原类相同的接口。梅耶的定义提倡实现继承。具体实现可以通过继承方式来重用,但是接口规格不必如此。已存在的实现对于修改是封闭的,但是新的实现不必实现原有的接口。
在 20 世纪 90 年代,开闭原则被广泛的重新定义由于抽象化接口的使用,在这中间实现可以被改变,多种实现可以被创建,并且多态化的替换不同的实现。相比梅耶的使用方式,多态开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的,并且新的实现必须,至少,实现那个接口。
总的来说,实现开放-封闭原则的关键是抽象,通过抽象类实现对修改的封闭。
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
|
class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type==1)
drawRectangle(s);
else if (s.m_type==2)
drawCircle(s);
}
void drawRectangle(Rectangle r) {
...
}
void drawCircle(Circle c) {
...
}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
|
上述的例子就没有遵守 OCP,我们一旦要添加一个新的图形,那就不可避免地要修改GraphicEditor
的源代码。但是我们可以抽象一个draw
方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
class Shape {
abstract void draw();
}
class Rectangle extends Shape {
public void draw() {
...
}
}
|
这样我们添加一个新的图形可以在它的内部实现自己的draw
方法。
里氏替换原则(LSP)
里氏替换原则是 Barbara Liskov 提出的,这是一种面向对象的设计原则,即如果我们调用一个父类的方法可以成功,那么替换成子类调用也应该完全可以运行。
具体来说,就是子类必须有其父类具有的方法,可以添加,但是不能删除。且子类不能有更弱的不变量,其方法(相比与父类)的前置条件不能更强,后置条件不能更弱。
接口聚合原则(ISP)
接口聚合原则意味着客户端不应依赖于它们不需要的方法,要将过大的接口拆分为足够小的接口。接口隔离原则的目的是系统解开耦合,从而容易重构,更改和重新部署。总结其实就是「低耦合,高内聚」的一个具体体现。
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
|
// bad example
interface Worker {
void work();
void eat();
}
class ManWorker implements Worker {
public void work() {
// ...
}
public void eat() {
// ...
}
}
class RobotWorker implements Worker {
public void work() {
// ...
}
public void eat() {
// unable to eat
}
}
|
由于机器人不能吃,所以上面的Worker
是一个太“胖”的接口,所以要将Worker
拆分。
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
|
// good example
interface Workable {
void work();
}
interface Feedable {
void eat();
}
class ManWorker implements Workable , Feedable{
public void eat() {
// ...
}
public void work() {
// ...
}
}
class RobotWorker implements Workable {
public void work() {
// ...
}
}
|
依赖转置原则(DIP)
依赖反转原则(DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。该原则规定:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
1
2
3
4
5
6
7
8
9
10
11
|
// bad example
void Copy(OutputStream dev) {
int c;
while ((c = ReadKeyboard()) != EOF) {
if (dev == printer) {
writeToPrinter(c);
} else {
writeToDisk(c);
}
}
}
|
在实现委托时,我们应当调用接口,而非具体的类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// good example
interface Reader {
public int read();
}
interface Writer {
public int write(c);
}
class Copy {
void Copy(Reader r, Writer w) {
int c;
while ((c = r.read()) != EOF) {
w.write(c);
}
}
}
|