面向对象编程的五个原则:SOLID

在程序设计领域, 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);
      }
  }
}
comments powered by Disqus