[譯]每一個開發者須要知道的 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

相關文章
相關標籤/搜索