[译]每一个开发者须要知道的 SOLID 原则

面向对象的编程方式给软件开发带来了新的设计方法。git

这使开发人员可以将具备相同目的/功能的数据聚合到一个类中,以达到该类要实现的惟一目的或功能,而无论应用程序总体上要作什么。github

可是,这种面向对象的编程方式并不能彻底防止开发者写出难以理解或难以维护的程序。 所以,Robert C. Martin提出了五项基本原则。这五条原则使开发人员很容易写出可读性高和更好维护的程序。数据库

这五个原则被称为 S.O.L.I.D 原则(该缩写由 Michael Feathers 提出)。编程

  • S:单一职责原则(Single Responsibility Principle)
  • O:开闭原则(Open-Closed Principle)
  • L:里氏替换原则(Liskov Substitution Principle)
  • I:接口隔离原则(Interface Segregation Principle)
  • D:依赖倒置原则(Dependency Inversion Principle)

接下来,咱们来详细论述这五个原则。数组

注意:本文中的大多数示例可能不足以知足实际状况,或者不适用于实际应用。这彻底取决于你本身的设计和用例。但最重要的事情是理解和知道如何应用和遵循这些原则。bash

小贴士:用相似Bit这样的工具把 SOLID 原则应用于实践。它能够帮助你组织、发现和重用组件从而来组成新的应用程序。组件能够在项目之间被发现和共享,所以你能够更快地构建项目。Git 地址网络

单一职责原则 (Single Responsibility Principle)

你只有一项工做。 —— 洛基,《雷神托尔:诸神黄昏》
一个类只应该有一项职责函数

一个类应该只负责作一件事情。若是一个类有多个职责,那么这多个职责就被耦合在了一块儿。一个功能发生变动会引发另外一个功能发生不指望的变动。微服务

  • 注意:此原则不只适用于类,还适用于组件开发和微服务。

例以下面的设计:工具

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
  saveAnimal(a: Animal) { }
}
复制代码

Animal 类违反了单一职责原则。

它为何违反了单一职责原则?

单一职责原则规定,一个类应该只有一个职责,但这里咱们能够看出两个职责: animal 数据库管理和 animal 属性管理。constructorgetAnimalName 方法管理 animal 的属性,而 saveAnimal 方法管理数据库中的 animal 存储。

这个设计在未来可能引发什么问题?

若是程序的变动须要影响到数据库管理功能,那么全部用到 animal 属性的类必须被修改并从新编译以兼容新的变化,

如今你能够感觉到这个系统有股死板的味道,就像多米诺骨牌效应,触摸一张牌,它会影响到全部其余牌。

为了使这个设计符合单一职责原则,咱们要建立另外一个类,该类将专门负责将 animal 对象存储到数据库中:

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
}
//  animalDB专门负责在数据库中读写animal
class AnimalDB {
  getAnimal(a: Animal) { }
  saveAnimal(a: Animal) { }
}
复制代码

在咱们设计类时,咱们应该把相关的 feature 放在一块儿,因此每当它们倾向于改变时,它们都会由于相同的缘由而改变。若是 feature 因不一样缘由发生变化,咱们应该尝试将它们分开。--Steve Fenton

经过适当地应用这些设计,咱们的应用程序将变得高度内聚。

开放-封闭原则 (Open-Closed Principle)

软件实体(类、模块、函数)等应当是易于扩展的,可是不可修改

让咱们继续看 Animal 类:

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
}
复制代码

咱们想要遍历一个animal数组,并让每一个animal发出对应的声音。

//...
const animals: Array<Animal> = [
  new Animal('lion'),
  new Animal('mouse')
];

function AnimalSound(a: Array<Animal>) {
  for (int i = 0; i <= a.length; i++) {
    if (a[i].name == 'lion') 
      log('roar');
    if(a[i].name == 'mouse')
      log('squeak');
  }
}
AnimalSound(animals);
复制代码

AnimalSound方法不符合开放-封闭原则,由于它没有对新类型的 animal 对象关闭。

若是咱们添加一个新的 animal 对象,Snake

//...
const animals: Array<Animal> = [
  new Animal('lion'),
  new Animal('mouse'),
  new Animal('snake')
]
//...
复制代码

咱们将不得不修改AnimalSound方法:

//...
function AnimalSound(a: Array<Animal>) {
  for (int i = 0; i <= a.length; i++) {
    if(a[i].name == 'lion')
      log('roar');
    if(a[i].name == 'mouse')
      log('squeak');
    if(a[i].name == 'snake')
      log('hiss');
    }
}
AnimalSound(animals);
复制代码

如今你能够感觉到,每新增一个 animal,就须要增长一段新的逻辑到 AnimalSound 方法中。这是一个很是简单的例子,当你的程序变得庞大而复杂时,你将看到每次添加新的animal时,if语句都会在AnimalSound方法中反复重复,直到充满整个应用。

那怎样让AnimalSound方法听从开闭原则呢?

class Animal {
  makeSound();
  //...
}

class Lion extends Animal {
  makeSound() {
    return 'roar';
  }
}

class Squirrel extends Animal {
  makeSound() {
    return 'squeak';
  }
}

class Snake extends Animal {
  makeSound() {
    return 'hiss';
  }
}

//...
function AnimalSound(a: Array<Animal>) {
  for(int i = 0; i <= a.length; i++) {
    log(a[i].makeSound());
  }
}
AnimalSound(animals);
复制代码

Animal 类如今有了一个虚方法( virtual method ) —— makeSound。咱们让每只 animal 继承了 Animal 类并实现了父类的makeSound方法。

每一个 animal 子类添加并在本身内部实现了 makeSound 方法。在 AnimalSound 方法遍历 animal 对象数组时,只须要调用每一个 animal 对象自身的 makeSound 方法便可。

如今,若是咱们新增一个animalAnimalSound方法不须要作出任何修改。咱们须要作的仅仅是把新增的这个 animal 对象加入到数组当中。

如今 AnimalSound 方法听从了开放-封闭原则。

再看一个例子:

假设你有一家店铺,并且你要经过这个 Discount 类给你喜欢的客户一个 2 折的折扣。

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}
复制代码

当你决定为 VIP 客户提供双倍的 20%折扣时。你能够这样修改 Discount 类:

class Discount {
  giveDiscount() {
    if(this.customer == 'fav') {
      return this.price * 0.2;
    }
    if(this.customer == 'vip') {
      return this.price * 0.4;
    }
 }
}
复制代码

不,这个设计违反了开放-封闭原则,开放-封闭原则禁止这么去作。若是咱们想给另外一种不一样类型的客户一个新的百分比折扣,你将添加一个新的逻辑。

为了使它可以遵循开放-封闭原则,咱们将添加一个新的类来扩展 Discount 类。在这个新类中,咱们将实现它的新行为:

class VIPDiscount: Discount {
  getDiscount() {
    return super.getDiscount() * 2;
  }
}
复制代码

若是你打算给超级 VIP 客户8折的折扣,咱们能够再新加一个 SuperVIPDiscount 类:

class SuperVIPDiscount: VIPDiscount {
  getDiscount() {
    return super.getDiscount() * 2;
  }
}
复制代码

如今你能够感觉到,在不作修改的状况下,咱们实现了功能的扩展。

里氏替换原则 (Liskov Substitution Principle)

子类必须能够替代它的父类。

这一原则的目的是肯定一个子类能够毫无错误地替代它的父类。让全部使用基类的地方都能透明地使用子类,若是代码在在检查类的类型,那么它必定违反了这个原则。

咱们继续使用 Animal 类的例子:

//...
function AnimalLegCount(a: Array<Animal>) {
  for(int i = 0; i <= a.length; i++) {
    if(typeof a[i] == Lion)
      log(LionLegCount(a[i]));
    if(typeof a[i] == Mouse)
      log(MouseLegCount(a[i]));
    if(typeof a[i] == Snake)
      log(SnakeLegCount(a[i]));
    }
}
AnimalLegCount(animals);
复制代码

这段代码违反了理氏替换原则(也违背了开放-封闭原则)——它必须直到每一个 Animal 对象的具体类型,并调用该对象所关联的 leg-counting 函数。

每新增一种 Animal 类,这个方法就必须做出修改,从而接收新的类型的 Animal 对象。

//...
class Pigeon extends Animal {

}

const animals[]: Array<Animal> = [
  //...,
  new Pigeon();
]

function AnimalLegCount(a: Array<Animal>) {
  for(int i = 0; i <= a.length; i++) {
    if(typeof a[i] == Lion)
      log(LionLegCount(a[i]));
    if(typeof a[i] == Mouse)
      log(MouseLegCount(a[i]));
    if(typeof a[i] == Snake)
      log(SnakeLegCount(a[i]));
    if(typeof a[i] == Pigeon)
      log(PigeonLegCount(a[i]));
  }
}
AnimalLegCount(animals);
复制代码

为了使该方法遵循里氏替换原则,咱们将遵循 Steve Fenton 假定的里氏替换原则的几项要求:

  • 若是父类(Animal)具备一个能够接收父类类型(Animal)做为参数的方法。它的子类(Pigeon)应该接受一个父类类型(Animal)或子类类型(Pigeon)做为参数。
  • 若是父类返回父类类型(Animal)。那么它的子类应该返回一个父类类型(Animal)或子类类型(Pigeon)。

如今,咱们来从新实现 AnimalLegCount 方法:

function AnimalLegCount(a: Array<Animal>) {
  for(let i = 0; i <= a.length; i++) {
    a[i].LegCount();
  }
}
AnimalLegCount(animals);
复制代码

AnimalLegCount 方法不关心传递的 Animal 对象的具体类型,它只调用 LegCount 方法。它只知道参数必须是 Animal 类型,能够是 Animal 类的实例,或者是它的子类的实例。

如今咱们须要在 Animal 类中定义 LegCount 方法:

class Animal {
  //...
  LegCount();
}
复制代码

同时它的子类须要实现 LegCount 方法:

//...
class Lion extends Animal{
  //...
  LegCount() {
    //...
  }
}
//...
复制代码

当 lion(Lion 的一个实例)被传递到 AnimalLegCount 方法中时,方法会返回 lion 拥有的腿数。

如今你能够感觉到,AnimalLegCount 方法不须要知道接收到的是什么类型的 animalAnimal子类的实例)就能够计算出它的腿数,由于它只须要调用Animal子类的实例的LegCount方法。依照契约,Animal的子类必须实现LegCount方法。

接口隔离原则 (Interface Segregation Principle)

为特定用户创造精心设计的接口。
不能强迫用户去依赖那些他们不使用的接口。

这个原则能够用来解决实现接口过于臃肿的缺点。

咱们看一下下面这个 IShape 接口:

interface IShape {
  drawCircle();
  drawSquare();
  drawRectangle();
}
复制代码

这个接口定义了绘制正方形、圆形、矩形的方法。实现 IShape 接口的类 CircleSquareRectangle都必 须实现方法 drawCircledrawSquaredrawRectangle

class Circle implements IShape {
  drawCircle() {
    //...
  }
  drawSquare() {
    //...
  }
  drawRectangle() {
    //...
  }
}
class Square implements IShape {
  drawCircle() {
    //...
  }
  drawSquare() {
    //...
  }
  drawRectangle() {
    //...
  }
}
class Rectangle implements IShape {
  drawCircle() {
    //...
  }
  drawSquare() {
    //...
  }
  drawRectangle() {
    //...
  }
}
复制代码

上面的代码看起来有点搞笑,Rectangle 类实现了它根本用不上的方法(drawCircledrawSquare),一样的,Square类也实现了 drawCircledrawRectangle 方法,以及Circle类(drawSquaredrawRectangle)。

若是咱们在IShape接口中添加一个方法,好比绘制三角形 drawTriangle

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
    drawTriangle();
}
复制代码

全部实现IShape接口的类都须要实现这个新增的方法,否则就会报错。

咱们看到,用这个设计不可能实例化一个能够画圆(drawCircle)但不能画矩形(drawRectangle)、正方形(drawSquare)或三角形(drawTriangle)的shape对象。咱们只能实现接口中全部的方法,而且在违反逻辑的方法中抛出一个操做没法执行的错误。

接口隔离原则不推荐这个 IShape 接口的设计。用户(这里指RectangleCircleSquare类)不该该被强制地依赖于它们不须要或不使用的方法。另外,接口隔离原则指出,接口应该只负责单一职责(就像单一职责原则),任何额外的行为都应该被抽象到另外一个接口。

在这里,咱们的IShape接口执行的操做应该由其余接口独立处理。

为了使咱们的IShape接口符合接口隔离原则,咱们将操做分离到不一样的接口:

interface IShape {
  draw();
}

interface ICircle {
  drawCircle();
}

interface ISquare {
  drawSquare();
}

interface IRectangle {
  drawRectangle();
}

interface ITriangle {
  drawTriangle();
}

class Circle implements ICircle {
  drawCircle() {
    //...
  }
}

class Square implements ISquare {
  drawSquare() {
    //...
  }
}

class Rectangle implements IRectangle {
  drawRectangle() {
    //...
  }
}

class Triangle implements ITriangle {
  drawTriangle() {
    //...
  }
}
class CustomShape implements IShape {
  draw(){
    //...
  }
}
复制代码

ICircle接口只负责绘制圆形,IShape负责绘制任何形状,ISquare只负责绘制正方形,IRectangle负责绘制矩形。

或者

子类(CircleRectangleSquareTriangle等)从 IShape 接口继承并实现本身的 draw 方法。

class Circle implements IShape {
  draw(){
    //...
  }
}

class Triangle implements IShape {
  draw(){
    //...
  }
}

class Square implements IShape {
  draw(){
    //...
  }
}

class Rectangle implements IShape {
  draw(){
    //...
  }
}
复制代码

而后咱们可使用I-接口来建立特定的 Shape 实例,如半圆(Semi Circle)、直角三角形(Right-Angled Triangle)、等边三角形(Equilateral Triangle)、梯形(Blunt-Edged Rectangle)等。

依赖倒置原则 (Dependency Inversion Principle)

依赖应该基于抽象,而不是基于具体的实现
A:高级模块不该依赖于低级模块。二者都应该依赖于抽象。
B:抽象不该依赖于细节,细节应该依赖于抽象。

在软件开发中咱们会遇到程序主要由模块组成的状况。当这种状况发生时,咱们不得不使用依赖注入来解决问题。高级组件依赖于低级组件。

class XMLHttpService extends XMLHttpRequestService {}

class Http {
  constructor(private xmlhttpService: XMLHttpService) { }
  
  get(url: string , options: any) {
    this.xmlhttpService.request(url,'GET');
  }
  
  post() {
    this.xmlhttpService.request(url,'POST');
  }
  //...
}
复制代码

这里,Http 是高级组件,而 HttpService 是低级组件。这种设计违反了依赖倒置原则:

A:高级模块不该依赖于低级模块。它应该依赖于抽象。

Http 类被强制依赖 于XMLHttpService 类。若是咱们要更改 Http 链接服务,多是须要经过 Nodejs 链接,或者是模拟 Http 服务。为了编辑代码,咱们将不得不费力地检查 Http 的全部实例,这违反了开放-封闭原则。

Http 类不该该太关注你正在使用的 Http 服务的类型。让咱们来创建一个 Connection 接口:

interface Connection {
    request(url: string, opts:any);
}
复制代码

Connection 接口有一个 request 方法。经过这个设计,咱们将 Connection 的实例做为参数传递给咱们的 Http 类:

class Http {
  constructor(private httpConnection: Connection) { }
  
  get(url: string , options: any) {
    this.httpConnection.request(url,'GET');
  }
  
  post() {
    this.httpConnection.request(url,'POST');
  }
    //...
}
复制代码

如今不管传递给Http 类的 Http 链接服务类型是什么,它均可以轻松地链接到网络,而没必要费心去了解网络链接的类型。

咱们如今能够从新实现 XMLHttpService 类来实现 Connection 接口:

class XMLHttpService implements Connection {
  const xhr = new XMLHttpRequest();
  //...
  request(url: string, opts:any) {
    xhr.open();
    xhr.send();
  }
}
复制代码

咱们能够建立不少 Http Connection 类型并将其传递给咱们的 Http 类,而没必要担忧报错。

class NodeHttpService implements Connection {
  request(url: string, opts:any) {
    //...
  }
}

class MockHttpService implements Connection {
  request(url: string, opts:any) {
    //...
  }
}
复制代码

如今,咱们能够看到高级模块和低级模块都依赖于抽象。Http 类(高级模块)依赖于 Connection 接口(抽象),反过来 Http 服务类型(低级模块)也依赖于 Connection 接口(抽象)。

此外,依赖倒置原则迫使咱们不要违反里氏替换原则:Connection 类型 Node-XML-MockHttpService 是能够透明替换其父类 Connection 的。

总结

在这里,咱们介绍了每一个软件开发者必须遵照的五个原则。作出改变一般都是痛苦的,但随着稳定的实践和坚持,它将成为咱们的一部分,并将对咱们的程序维护工做产生巨大的影响。

参考:

原文地址

SOLID Principles every Developer Should Know , Chidume Nnamdi

相关文章
相关标签/搜索