SOLID 原则是五个面向对象程序设计基本原则的首字母简写,应用 SOLID 原则可以帮助我们开发出更容易维护、更灵活的软件。从而,在软件规模逐渐变大的时候,降低软件的复杂度,节省开发维护成本。

SOLID 原则由如下五个原则组成:

  • 单一职责原则(Single Responsibility)
  • 开闭原则(Open/Closed 原则)
  • 里氏替换原则(Liskov Substitution)
  • 接口隔离原则(Interface Segregation)
  • 依赖倒置原则(Dependency Inversion)

看名字有些原则可能不太好理解,下面我们通过一些代码示例来逐一说明。

单一职责原则(Single Responsibility Principle)

通过名字就可以知道,这个原则的含义是一个类应该只有一种职责。换句话说,我们只能有一个原因来改变这个类。

应用了单一职责的类,有如下几个优点:

  • 易测试。只有单一职责的类需要的测试 case 非常少,非常容易测试。
  • 低耦合。一个类的功能越少,需要的外部依赖越少。
  • 易管理。功能单一的类比功能复杂的类更容易进行组织。

举个例子,我们有一个 Book 类:

class Book {
  private name: string;
  private author: string;
  private text: string;
}
1
2
3
4
5

在这个类中,我们保存了书名、作者和书的文本三个字段。

现在我们添加一些操作文本的函数。

class Book {
  private name: string;
  private author: string;
  private text: string;

  replaceWordInText(word: string): string {
    return text.replaceAll(word, text);
  }

  isWordInText(word: string): boolean {
    return text.indexOf(word) !== -1;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

现在假如说我们要把书的内容输出到控制台,我们可以给 Book 类加一个 print 方法:

    // ....
    printTextToConsole() {
        // 输出内容函数实现
    }
1
2
3
4

这个时候,新添加的方法就破坏了单一职责原则。

我们应该再定义一个类,BookPrinter 来专门输出书的内容:

class BookPrinter {
  printTextToConsole(text: string) {
    //输出到控制台
  }

  printTextToAnotherMedium(text: string) {
    // 输出到其他媒体类型
  }
}
1
2
3
4
5
6
7
8
9

通过 BookPrinter 类,我们不断实现了输出书的内容到控制台的功能,我们还可以支持将书的内容输出到其他媒体类型。不管是输出到邮件、日志还是什么其他地方,我们都有一个单独的类来处理这个问题。

开闭原则(Open/Closed Principle)

开闭原则可以描述为”对扩展开放,对修改关闭“。也就是说,一个类只应该对扩展开放,对修改应该是关闭的。

我们通过一个例子来说明。假设我们有一个生产电脑的类:

class MacOSComputer {}

class WindowsComputer {}

class ComputerFactory {
  computerTypes = ["macos", "windows"];

  createComputer(type) {
    switch (type) {
      case "macos":
        return new MacOSComputer();
      case "windows":
        return new WindowsComputer();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

现在,假如我们需要创建一个 linux 系统的电脑,我们应该怎么办呢?最直接的办法是修改 ComputerFactory 这个类,来支持新电脑的创建。但是这么做我们可能会影响到其他两种类型电脑的创建。结合开闭原则,我们可以做一些改造。

abstract class ComputerFactory {
  createComputer() {}
}

class MacOSFactory extends ComputerFactory {
  createComputer() {
    return new MacOSComputer();
  }
}

class WindowsFactory extends ComputerFactory {
  createComputer() {
    return new WindowsComputer();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这个时候,如果我们新增一个 Linux 类型的电脑,就非常的方便。

class LinuxComputer {}

class LinuxFactory extends ComputerFactory {
  createComputer() {
    return new LinuxComputer();
  }
}
1
2
3
4
5
6
7

里氏替换原则(Liskov Substitution Principle)

里氏替换原则则简单的说,就是如果类 A 是类 B 的子类,那么我们可以在系统内将 B 替换成 A,且不改变软件行为。

我们还是通过例子来说明。

interface Car {
  turnOnEngine(): void;
  accelerate(): void;
}
1
2
3
4

我们定义了一个 Car 接口,提供了一些函数需要实现类来实现。

class MotorCar implements Car {
  private engine: Engine;

  turnOnEngine() {
    this.engine.on();
  }

  accelerate() {
    this.engine.powerOn(1000);
  }
}
1
2
3
4
5
6
7
8
9
10
11

假如现在需要实现一个电动车:

class ElectricCar implements Car {
  turnOnEngine() {
    throw new Error("No engine");
  }

  accelerate() {
    //
  }
}
1
2
3
4
5
6
7
8
9

这时候,如果我们将新实现的电动车放到系统中的话,就会改变系统的行为,导致出错。

我们需要重新定义一个没有引擎的 Car 接口:

interface Car {
  accelerate(): void;
}
1
2
3

接口隔离原则(Interface Segregation Principle)

接口隔离原则要求我们将大的接口拆分成小的接口,这样可以保证实现类能聚焦在它需要关心的接口上面。

我们以动物饲养员为例。

interface BearKeeper {
  washTheBear(): void;
  feedTheBear(): void;
  petTheBear(): void;
}
1
2
3
4
5

假如现在有一个熊饲养员只关心 washTheBearpetTheBear,那么在现在的接口设计中,还需要被迫实现 petTheBear

我们可以把这个大的接口进行拆分。

interface BearCleaner {
  washTheBear(): void;
}

interface BearFeeder {
  feedTheBear(): void;
}

interface BearPetter {
  petTheBear(): void;
}
1
2
3
4
5
6
7
8
9
10
11

那么这时候不同的熊饲养员就可以自由的实现关心的接口了。

class BearCarer implements BearCleaner, BearFeeder {
  washTheBear() {}
  feedTheBear() {}
}
1
2
3
4

在前面单一职责的例子中,我们还可以将 BookPrinter 进行拆分。

依赖倒置原则(Dependency Inversion Principle)

依赖倒置原则主要用于模块间的解耦。通常说,应用了依赖倒置原则的设计,高层模块将不会再直接依赖低层模块,而是依赖于抽象。

举个例子。我们有一个 Computer 类:

class Computer {
  private keyboard: StandardKeyboard;

  constructor() {
    this.keyboard = new StandardKeyboard();
  }
}
1
2
3
4
5
6
7

在 Computer 的构造函数中,我们将 StandardKeyboard 类和 Computer 类耦合在了一起。现在加入我们要新增一种键盘,那 Computer 的构造函数将会难以处理。

使用依赖倒置的方法,我们可以抽象出一个 Keyboard 接口来进行解耦:

interface Keyboard {}

class Computer {
  private keyboard: Keyboard;

  constructor(keyboard: Keyboard) {
    this.keyboard = keyboard;
  }
}
1
2
3
4
5
6
7
8
9

这时候重新实现 StandardKeyboard 类:

class StandardKeyboard implements Keyboard {}
1

重构后的代码,实现了 StandardKeyboard 和 Computer 的解耦,我们可以根据需要自由实现 Keyboard 接口。

小结

SOLID 原则是面向对象程序设计中非常经典的设计原则,同时也非常简单。理解和掌握这些原则,不仅对我们日常的开发维护工作帮助很大,也能提高自身的软件设计能力。

参考文献

关注微信公众号,获取最新推送~