S.O.L.I.D 原则
- 由 Robert C. Martin 制定
- 由 Michael Feathers 派生出的首字母缩写词
- 针对 OOP (面向对象编程) 制定的设计理念
- 旨在使开发人员可以轻松创建可读和可维护的程序
S.O.L.I.D 本意
Short Spell | Full Spell | Chinese Translation |
---|---|---|
S | Single Responsibility Principle | 单一职责原则 |
O | Open-Closed Principle | 开放封闭原则 |
L | Liskov Substitution Principle | 里氏替换原则 |
I | Interface Segregation Principle | 接口隔离原则 |
D | Dependency Inversion Principle | 依赖倒置原则 |
Single Responsibility Principle 单一职责原则
一个类应该只负责一个职责。如果一个类有多个职责,那么它就会变得耦合。修改一项职责会导致要去修改另一项职责。单一职责原则不仅适用于类,也适用于组件和微服务
class Hero {
constructor(name: string) {}
getHeroName() {}
registerHero(hero: Hero) {}
}
Hero 类违反了 SRP。SRP 声明类应该有一个责任,在这里 Hero 类拥有 2 个职责:管理 Hero 的属性与管理 Hero 的数据。
带来的问题
如果程序以影响数据库管理功能的方式更改,必须触及并重新编译使用 Hero 属性的类以抵偿新的更改,时间越久远越容易引发多米诺效应,影响到整个程序链路。
代码的调整
class Hero {
constructor(name: string) {}
getHeroName() {}
}
class HeroDB {
getHero(hero: Hero) {}
setHero(hero: Hero) {}
}
名言的引用
我们在设计类的时候,应该将相关的功能放在一起,每当要变化的时候,它们会出于同样的原因而改变。如果功能因不同原因而发生变化,我们应该尝试将功能分开。 —— 史蒂夫芬顿
Open-Closed Principle 开放封闭原则
软件实体(类,模块,函数)应该是可以扩展的,而不是修改。
class Hero {
constructor(name: string) {}
whichHero(heroes: Array) {
const heroesArr = ["SuperMan", "IronMan"];
for (hero of heroes) {
if (heroesArr.inclues(hero.name)) console.log(`I am ${hero.name}!`);
}
}
}
带来的问题
whichHero 方法不符合开放封闭原则,因为它不能对新创造的 Hero 进行封闭。如果我们新创造另一个 Hero,就必须对 whichHero 进行修改。
class Hero {
constructor(name: string) {}
whichHero(heroes: Array) {
const heroesArr = ["SuperMan", "IronMan", "AntMan"];
for (hero of heroes) {
if (heroesArr.inclues(hero.name)) console.log(`I am ${hero.name}!`);
}
}
}
每次新增一个 Hero,whichHero 函数都需要添加新的数组元素到 heroesArr 变量中。当然这里只是简单地添加数组元素,但当应用程序应对的是更为复杂的逻辑并且有不断增长的趋势,我们将应对的不仅仅是添加新的数组元素到 heroesArr 变量中这么简单了。
代码的调整
class Hero {
constructor(name: string) {}
whichHero() {}
}
class SuperMan extends Hero {
whichHero() {
console.log(`I am ${this.name}`);
}
}
class IronMan extends Hero {
whichHero() {
console.log(`I am ${this.name}`);
}
}
class AntMan extends Hero {
whichHero() {
console.log(`I am ${this.name}`);
}
}
现在 Hero 有一个虚拟方法 whichHero,供不同的 Hero (Like SuperMan)实现各自的虚拟 whichHero 方法。这样各子类不仅能实现 whichHero,并且能自由地重载 whichHero,不需要像原来一样还得往 heroesArr 中添加新的数组元素来进行修改。
Liskov Substitution Principle 里氏替换原则
子类必须可以替换其父类,任何父类可以出现的地方,子类也一定可以出现。
假如一个能提供定制的功能给终端用户的大型门户网站,系统根据用户的级别提供不同级别的设定,设计一个 ISettings 接口以及实现 ISettings 的 GlobalSettings、SectionSettings UserSettings 3 个类。GlobalSettings 为全局设定,如标题、主题等,SectionSettings 为局部设定,如新闻、天气、体育等,UserSettings 为用户设定,如电子邮件、通知偏好等。
interface ISettings {
get(): void;
set(): void;
}
class GlobalSettings implements ISettings {
get(): void {}
set(): void {}
}
class SectionSettings implements ISettings {
get(): void {}
set(): void {}
}
class UserSettings implements ISettings {
get(): void {}
set(): void {}
}
带来的问题
当单独使用 GuestSettings 时,因为我们了解游客不能设置,所以我们潜意识并不会主动调用 set 方法。但是由于多态,ISettings 接口的实现可以被替换为 GuestSettings 对象,当调用 set 方法时,可能会引发系统异常。
代码的调整
将 ISettings 拆分为两个不同的接口:IReadableSettings 与 IWritableSettings。子类根据需求实现所需的接口。
interface IReadableSettings {
get(): void;
}
interface IWritableSettings {
set(): void;
}
class GlobalSettings implements IReadableSettings, IWritableSettings {
get(): void {}
set(): void {}
}
class SectionSettings implements IReadableSettings, IWritableSettings {
get(): void {}
set(): void {}
}
class UserSettings implements IReadableSettings, IWritableSettings {
get(): void {}
set(): void {}
}
class GuestSettings implements IReadableSettings {
get(): void {}
}
现在 GuestSettings 就只实现了 IReadableSettings 的 get 方法,不像原来一样还需要实现 ISettings 的 set 方法,确保系统不抛出异常也提供给游客的基本操作
Interface Segregation Principle 接口隔离原则
制作客户特定的细粒度接口不应强迫客户端依赖于他们不使用的接口。
interface IShape {
drawCircle(): void;
drawSquare(): void;
drawRectangle(): void;
}
class Circle implements IShape {
drawCircle() {}
drawSquare() {}
drawRectangle() {}
}
class Square implements IShape {
drawCircle() {}
drawSquare() {}
drawRectangle() {}
}
class Rectangle implements IShape {
drawCircle() {}
drawSquare() {}
drawRectangle() {}
}
带来的问题
此接口绘制正方形,圆形,矩形。实现 IShape 接口的类 Circle,Square 或 Rectangle 都必须定义 drawCircle(),drawSquare(),drawRectangle()方法。类 Rectangle 实现了它没有使用的方法 drawCircle 与 drawSquare,类 Square 实现了它没有使用的方法 drawCircle 与 drawRectangle,类 Circle 实现了它没有使用的方法 drawSquare 与 drawRectangle。如果此时向 IShape 接口添加另一个方法,比如 drawTriangle()
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}
类必须实现新方法 drawTriangle()否则将抛出错误。实现一个可以绘制圆形而不实现绘制矩形,正方形与三角形的形状是不可能的。IShape 接口的设计不符合接口隔离原则,类 Circle,Square 与 Rectangle 不应强制依赖于它们不需要或不使用的方法。此外,ISP 声明接口应该只执行一件事情(就像 SRP 原则一样),任何额外的行为都应该被抽象到另一个接口。因此 IShape 接口的执行应由其他接口独立处理操作。
代码的调整
interface IShape {
draw();
}
interface ICircle {
drawCircle();
}
interface ISquare {
drawSquare();
}
interface IRectangle {
drawRectangle();
}
interface ITriangle {
drawTriangle();
}
class Shape implements IShape {
draw() {}
}
class Circle implements ICircle {
drawCircle() {}
}
class Square implements ISquare {
drawSquare() {}
}
class Rectangle implements IRectangle {
drawRectangle() {}
}
class Triangle implements ITriangle {
drawTriangle() {}
}
IShape 接口可以绘制任何形状, ICircle 接口只绘制圆形,ISquare 接口只绘制正方形,IRectangle 接口只绘制矩形,ITriangle 接口只绘制三角形。
Or
类 Circle,Square,Rectangle 与 Triangle 可以从 IShape 接口继承并实现它们自己的绘制行为。
interface IShape {
draw();
}
class Circle implements IShape {
draw() {}
}
class Square implements IShape {
draw() {}
}
class Rectangle implements IShape {
draw() {}
}
class Triangle implements IShape {
draw() {}
}
这样当需要绘制其他图形时只需要从 IShape 接口继承并实现其绘制行为即可。
Dependency Inversion Principle 依赖倒置原则
依赖应该基于抽象而不是具体,通常采用依赖注入的方法来解决违反依赖倒置原则的情况。
- 抽象不应依赖于具体。具体应取决于抽象
- 高级模块不应依赖于低级模块。两者都应依赖于抽象
class XMLHttpRequestService {
request(url: string, options: any) {}
}
class XMLHttpService extends XMLHttpRequestService {}
class Http {
constructor(private xmlhttpService: XMLHttpService) {}
get(url: string, options: any) {
this.xmlhttpService.request(url, options);
}
post(url: string, options: any) {
this.xmlhttpService.request(url, options);
}
}
带来的问题
这里 Http 是高级组件,而 XMLHttpService 是低级组件。此设计违反了 DIP 原则(高级模块不应依赖于低级模块。它应该取决于它的抽象),Http 类被迫依赖于 XMLHttpService 类。如果我们变更 Http 的连接服务类型为 Node.js 或者只是单纯地进行 Mock,就必须更改所有的 Http 实例代码,这也违反了 OCP 原则。Http 类应该更少关注正在使用的 Http 服务的类型。
代码的调整
interface Connection {
request(url: string, options: any);
}
class XMLHttpService implements Connection {
request(url: string, options: any) {}
}
class NodeHttpService implements Connection {
request(url: string, options: any) {}
}
class MockHttpService implements Connection {
request(url: string, options: any) {}
}
class Http {
constructor(private httpConnection: Connection) {}
get(url: string, options: any) {
this.httpConnection.request(url, options);
}
post(url: string, options: any) {
this.httpConnection.request(url, options);
}
}
现在无论传递给 Http 的连接服务的类型如何,它都可以轻松地进行网络请求,并且可以看到高级模块和低级模块都依赖于抽象——Http 类(高级模块)依赖于 Connection 接口(抽象),Http Service 类(低级模块)依赖于 Connection 接口(抽象)。此外,DIP 将强制我们不能违反 LSP:连接服务类型 (XML|Node|Mock)HttpService 可替换其父类型 Connection。
结论
S.O.L.I.D 原则是每个开发者都必须遵守的,需要开发者通过在不断的实践中遵守,最终它们将成为编程的一部分,并将使得应用程序的可读性与可维护性大大增强。