SOLID
Q7nl1s admin

概述

SOLID是面向对象编程和面向对象开发的五个基本原则(的记忆术首字母略写字),指代了这五个基本原则,当这些原则一起被应用使得软件维护和系统的扩展变得更加可能,便于程序设计者对软件源代码的重构和代码异味清扫。它被典型应用在测试驱动开发上 ,且为敏捷开发及自适应开发的基本原则的重要组成部分。

五个基本原则具体如下

首字母 指代 概念
S 单一功能原则 认为“对象应该仅具有一种单一功能”的概念。
O 开闭原则 认为“软件应该是对于扩展开放的,但是对于修改封闭的”的概念。
L 里氏替换原则 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。参考契约式设计
I 接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口” 的概念。
D 依赖反转原则 认为一个方法应该遵从“依赖于抽象而不是一个实例” 的概念。 依赖注入是该原则的一种实现方式。

在开始之前

在我们了解每个 SOLID 原则之前, 我们需要回忆软件开发中两个相关的概念。耦合和内聚:

耦合:我们可以把它定义为一个类、方法或者任何一个实体直接与另一个实体连接的度。这个耦合的度也可以被看作依赖的度。

  • 例子:当我们想要使用的一个类,与一个或者多个类紧密地绑定在一起(高耦合),我们将最终使用或修改这些类中我们所依赖的部分。

内聚:内聚是一个系统里两个或多个部分一起执行工作的度量,来获得比每个部分单独工作获得更好的结果。

  • 例子: 星球大战中 Han Solo 和 Chewbacca 一起在千年隼号里。

想要有一个高质量的软件,我们必须尝试低耦合高内聚,而 SOLID 原则正好帮助我们完成这个任务。如果我们遵循这些指引,我们的代码会更健壮,更易于维护,有更高的复用性和可扩展性。同时,可以避免每次变更都要修改多处代码的问题。


单一功能原则

在面向对象编程领域中,单一功能原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

违反 SRP 原则
  • 我们的 Customer 类有多个的职责:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Customer {

private String name;

// getter and setter methods...

// This is a Responsibility
public void storeCustomer(String customerName) {
// store customer into a database...
}

// This is another Responsibility
public void generateCustomerReport(String customerName) {
// generate a report...
}
}

storeCustomer(String name) 职责是把顾客存入数据库。这个职责是持续的,应该把它放在顾客类的外面。

generateCustomerReport(String name) 职责是生成一个关于顾客的报告,所以它也应该放在顾客类的外面。

当一个类有多个职责,它就更加难以被理解,扩展和修改。

更好的解决办法:

我们 为每一个职责创建不同的类。

  • Customer 类:
1
2
3
4
5
6
public class Customer {

private String name;

// getter and setter methods...
}
  • CustomerDB 类用于持续的职责:
1
2
3
4
5
6
public class CustomerDB {

public void storeCustomer(String customerName) {
// store customer into a database...
}
}
  • CustomerReportGenerator 类用于报告制作的职责:
1
2
3
4
5
6
public class CustomerReportGenerator {

public void generateReport(String customerName) {
// generate a report...
}
}

这样,我们就有几个类,但是每个类都有单一的职责,我们就使它变成了低耦合高内聚。


开闭原则

在面向对象编程领域中,开闭原则 (The Open/Closed Principle, OCP) 规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查单元测试以及诸如此类的用以确保产品使用品质的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

违反 OCP 原则
  • 我们有一个 Rectangle 类:
1
2
3
4
5
6
7
public class Rectangle {

private int width;
private int height;

// getter and setter methods...
}
  • 同时,我们有一个 Square 类
1
2
3
4
5
6
public class Square {

private int side;

// getter and setter methods...
}
  • 我们还有一个 ShapePrinter 类可以画不同的形状:
1
2
3
4
5
6
7
8
9
10
11
public class ShapePrinter {

public void drawShape(Object shape) {

if (shape instanceof Rectangle) {
// Draw Rectangle...
} else if (shape instanceof Square) {
// Draw Square...
}
}
}

可以看到,当我们每次想要画一个新的形状我们就要修改 ShapePrinter 类里的 drawShape 方法来接受这个新的形状。

当要画新的形状种类的时候,ShapePrinter 类就会变得更让人难以理解并且不易于改变。

所以 ShapePrinter 类不对修改封闭。

一个解决办法:
  • 我们添加一个 Shape 抽象类:
1
2
3
public abstract class Shape {
abstract void draw();
}
  • 重构 Rectangle 类以继承自 Shape:
1
2
3
4
5
6
7
8
9
10
11
12
public class Rectangle extends Shape {

private int width;
private int height;

// getter and setter methods...

@Override
public void draw() {
// Draw the Rectangle...
}
}

重构 Square 类以继承自 Shape:

1
2
3
4
5
6
7
8
9
10
11
public class Square extends Shape {

private int side;

// getter and setter methods...

@Override
public void draw() {
// Draw the Square
}
}
  • ShapePrinter 的重构:
1
2
3
4
5
6
public class ShapePrinter {

public void drawShape(Shape shape) {
shape.draw();
}
}

现在,ShapePrinter 类在我们添加了新的形状类型的同时也保持了完整性。

另一个解决方法:

用这个方法,ShapePrinter 也能在添加新形状的同时保持完整性,因为 drawShape 方法接受 Shape 抽象。

  • 我们把 Shape 变成一个接口:
1
2
3
public interface Shape {
void draw();
}
  • 重构 Rectangle 类以实现 Shape:
1
2
3
4
5
6
7
8
9
10
11
12
public class Rectangle implements Shape {

private int width;
private int height;

// getter and setter methods...

@Override
public void draw() {
// Draw the Rectangle...
}
}
  • 重构 Square 类以实现 Shape:
1
2
3
4
5
6
7
8
9
10
11
public class Square implements Shape {

private int side;

// getter and setter methods...

@Override
public void draw() {
// Draw the Square
}
}
  • ShapePrinter:
1
2
3
4
5
6
public class ShapePrinter {

public void drawShape(Shape shape) {
shape.draw();
}
}

里氏替换原则

在面向对象的程序设计中,里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。” 以上内容并非利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读。其原文为:

SOLID_0

违反 LSP 原则:
  • 我们有一个 Rectangle 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Rectangle {

private int width;
private int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}

public int getArea() {
return width * height;
}
}
  • 还有一个 Square 类:

因为一个正方形是一个长方形(从数学上讲),我们决定把 Square 作为 Rectangle 的子类。

我们在重写的 setHeight() 和 setWidth() 方法中设置(与它的父类)同样的尺寸(宽和高),让 Square 的实例依然有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Square extends Rectangle {

@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}

@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}

所以现在我们可以传一个 Square 实例到一个需要 Rectangle 实例的地方。

但是如果我们这样做,我们会破坏 Rectangle 的行为假设:

下面对于 Rectangle 的假设是对的:

1
2
3
4
5
6
7
8
9
10
11
12
public class LiskovSubstitutionTest {

public static void main(String args[]) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(2);
rectangle.setHeight(5);

if (rectangle.getArea() == 10) {
System.out.println(rectangle.getArea());
}
}
}

但是同样的假设却不适用于 Square:

1
2
3
4
5
6
7
8
9
10
11
12
public class LiskovSubstitutionTest {

public static void main(String args[]) {
Rectangle rectangle = new Square(); // Square
rectangle.setWidth(2);
rectangle.setHeight(5);

if (rectangle.getArea() == 10) {
System.out.println(rectangle.getArea());
}
}
}

Square 不是 Rectangle 正确的替代品,因为它不遵循 Rectangle 的行为规则。

Square / Rectangle 层次分离虽然不能反应出任何问题,但是这违反了里氏替换原则!

一个解决方法:
  • 用 Shape 接口来获取面积:
1
2
3
public interface Shape {
int area();
}
  • 重构 Rectangle 以实现 Shape:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Rectangle implements Shape {

private int width;
private int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}

@Override
public int area() {
return width * height;
}
}

重构 Square 以实现 Shape:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Square implements Shape {

private int size;

public void setSize(int size) {
this.size = size;
}

@Override
public int area() {
return size * size;
}
}
另一个解决方法经常与非可变性一起应用
  • Rectangle 重构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Rectangle {

private final int width;
private final int height;

public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}

public int getArea() {
return width * height;
}
}
  • 重构 Square 以继承 Rectangle:
1
2
3
4
5
6
public class Square extends Rectangle {

public Square(int side) {
super(side, side);
}
}

很多时候,我们对类的建模依赖于我们想展示的现实世界客体的属性,但更重要的是我们应该关注它们各自的行为来避免这种错误。


接口隔离原则

(英语:interface-segregation principles, 缩写:ISP)指明客户(client)不应被迫使用对其而言无用的方法或功能。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。接口隔离原则(ISP)的目的是系统解开耦合(即一程序中,模块及模块之间信息或参数依赖的程度),从而容易重构,更改和重新部署。接口隔离原则是在SOLID中五个面向对象设计(OOD)的原则之一,类似于在GRASP中的高内聚性(也称为内聚力,是一软件度量,是指机能相关的程序组合成一模块的程度)。

违反 ISP 原则
  • 我们有一个 Car 的接口:
1
2
3
4
public interface Car {
void startEngine();
void accelerate();
}
  • 同时也有一个实现 Car 接口的 Mustang 类:
1
2
3
4
5
6
7
8
9
10
11
12
public class Mustang implements Car {

@Override
public void startEngine() {
// start engine...
}

@Override
public void accelerate() {
// accelerate...
}
}

现在我们有个新的需求,要添加一个新的车型:

一辆 DeloRean, 但这并不是一个普通的 DeLorean,我们的 DeloRean 非常特别,它有穿梭时光的功能。

像以往一样,我们没有时间来做一个好的实现,而且 DeloRean 必须马上回到过去。

  • 为我们的 DeloRean 在 Car 接口里增加两个新的方法:
1
2
3
4
5
6
public interface Car {
void startEngine();
void accelerate();
void backToThePast();
void backToTheFuture();
}
  • 现在我们的 DeloRean 实现 Car 的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DeloRean implements Car {

@Override
public void startEngine() {
// start engine...
}

@Override
public void accelerate() {
// accelerate...
}

@Override
public void backToThePast() {
// back to the past...
}

@Override
public void backToTheFuture() {
// back to the future...
}
}
  • 但是现在 Mustang 被迫去实现在 Car 接口里的新方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Mustang implements Car {

@Override
public void startEngine() {
// 启动引擎
}

@Override
public void accelerate() {
// 加速
}

@Override
public void backToThePast() {
// 因为 Mustang 不能回到过去!
throw new UnsupportedOperationException();
}

@Override
public void backToTheFuture() {
// 因为 Mustang 不能穿越去未来!
throw new UnsupportedOperationException();
}
}

在这种情况下,Mustang 违反了接口隔离的原则,因为它实现了它不会用到的方法。

使用接口隔离的解决方法:
  • 重构 Car 接口:
1
2
3
4
public interface Car {
void startEngine();
void accelerate();
}
  • 增添一个 TimeMachine 接口:
1
2
3
4
public interface TimeMachine {
void backToThePast();
void backToTheFuture();
}
  • 重构 Mustang(只实现 Car 的接口)
1
2
3
4
5
6
7
8
9
10
11
12
public class Mustang implements Car {

@Override
public void startEngine() {
// 启动引擎
}

@Override
public void accelerate() {
// 加速
}
}
  • 重构 DeloRean(同时实现 Car 和 TimeMachine)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DeloRean implements Car, TimeMachine {

@Override
public void startEngine() {
// 启动引擎
}

@Override
public void accelerate() {
// 加速
}

@Override
public void backToThePast() {
// 回到过去
}

@Override
public void backToTheFuture() {
// 到未来去
}
}

依赖反转原则

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。

违反 DIP 原则:
  • 我们有一个类叫 DeliveryDriver 代表着一个司机为快递公司工作。
1
2
3
4
5
6
public class DeliveryDriver {

public void deliverProduct(Product product){
// 运送产品
}
}
  • DeliveryCompany 类处理货物装运:
1
2
3
4
5
6
7
public class DeliveryCompany {

public void sendProduct(Product product) {
DeliveryDriver deliveryDriver = new DeliveryDriver();
deliveryDriver.deliverProduct(product);
}
}

我们注意到 DeliveryCompany 创建并使用 DeliveryDriver 实例。所以 DeliveryCompany 是一个依赖于低层次类的高层次的类,这就违背了依赖倒转原则。(注:上述代码中 DeliveryCompany 需要运送货物,必须需要一个 DeliveryDriver 参与。但如果以后对司机有更多的要求,那我们既要修改 DeliveryDriver 也要修改上述代码。这样造成的依赖,耦合度高)

解决方法:
  • 我们创建 DeliveryService 接口,这样我们就有了一个抽象。
1
2
3
public interface DeliveryService {
void deliverProduct(Product product);
}
  • 重构 DeliveryDriver 类以实现 DeliveryService 的抽象方法:
1
2
3
4
5
6
7
public class DeliveryDriver implements DeliveryService {

@Override
public void deliverProduct(Product product) {
// 运送产品
}
}
  • 重构 DeliveryCompany,使它依赖于一个抽象而不是一个具体的东西。
1
2
3
4
5
6
7
8
9
10
11
12
public class DeliveryCompany {

private DeliveryService deliveryService;

public DeliveryCompany(DeliveryService deliveryService) {
this.deliveryService = deliveryService;
}

public void sendProduct(Product product) {
this.deliveryService.deliverProduct(product);
}
}

现在,依赖在别的地方创建,并且从类构造器中被注入。

千万不要把这个原则与依赖注入混淆。依赖注入是一种设计模式,帮助我们应用这个原则来确保各个类之间的合作不涉及相互依赖。

这里有好几个库使依赖注入更容易实现,像 Guice 或者非常流行的 Dagger2

文献参考:

SOLID 原则:权威指南-阿里云开发者社区 (aliyun.com)

依赖注入 - 维基百科,自由的百科全书 (wikipedia.org)

SOLID (面向对象设计) - 维基百科,自由的百科全书 (wikipedia.org)

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View