每一個開發者都應該知道的SOLID原則

每一個開發者都應該知道的SOLID原則

轉載自,原做者爲 Chidume Nnamdi。javascript

面向對象編程帶來了新的軟件開發設計方法。它使得開發人員可以將具備相同做用 / 功能的數據組合到一個類中,實現惟一的目的,而無論整個應用程序如何。java

可是,這種面向對象的編程並不能防止難以理解或不可維護的程序。所以,Robert C. Martin 制定了五項指導原則,使開發人員很容易建立出可讀性強且可維護的程序。這五項原則被稱爲 S.O.L.I.D 原則(這種縮寫是由 Michael Feathers 提出的):數據庫

  • S:單一職責原則
  • O:開閉原則
  • L:里氏替換原則
  • I:接口隔離原則
  • D:依賴倒置原則

下面咱們將展開詳細的討論。
注意:本文中的大多數示例可能不能知足實際狀況或不能應用於實際的應用程序。這徹底取決於你本身的設計和場景。最重要的是理解並知道如何應用 / 遵循這些原則
提示:SOLID 原則是爲構建模塊化、可擴展和可組合的封裝組件而設計的。編程

單一職責原則(SRP)

一個類只應該負責一件事。若是一個類有多個職責,那麼它變成了耦合的。對一個職責的修改會致使對另外一個職責的修改。
注意:這個原則不只適用於類,也適用於軟件組件和微服務。
例如,考慮下面的設計:數組

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}

上面的 Animal 就違反了單一職責原則(SRP)。網絡

它爲何違反了 SRP?

SRP 指出,類應該有一個職責,在這裏,咱們能夠得出兩個職責:動物數據庫管理和動物屬性管理。構造函數和 getAnimalName 管理動物屬性,而 saveAnimal 管理 Animal 在數據庫中的存儲。模塊化

這種設計未來會帶來什麼問題?

若是應用程序的修改影響了數據庫管理功能,使用 Animal 屬性的類就必須修改和從新編譯,以適應這種新的變化。這個系統就有點像多米諾骨牌,觸碰一張牌就會影響到其餘牌。函數

爲了使這個類符合 SRP,咱們建立了另外一個類,它負責將動物存儲到數據庫中這個單獨的職責:微服務

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}

在設計咱們的類時,咱們應該把相關的特性放在一塊兒,這樣,每當它們須要改變的時候,它們都是由於一樣的緣由而改變。若是它們因不一樣的緣由而改變,咱們就應該嘗試將它們分開。——Steve Fentonpost

恰當運用這條原則,咱們的應用程序就會變成高內聚的。

開閉原則(OCP)

軟件實體(類、模塊、函數)應該對擴展開放,對修改關閉。
讓咱們繼續以 Animal 類爲例。

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

咱們但願遍歷一個動物列表,發出它們的聲音。

//...
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 不符合開閉原則,由於它不能對新的動物關閉。若是咱們添加一種新的動物蛇:

//...
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);

如你所見,對於每一種新的動物,一段新的邏輯會被添加到 AnimalSound 函數。這是一個很是簡單的例子。當應用程序變得龐大而複雜時,你會看到,每添加一種新動物,if 語句就得在 AnimalSound 函數中重複一遍。

如何使它(AnimalSound)符合 OCP?

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 如今有了一個虛方法 makeSound。咱們讓每一種動物擴展 Animal 類並實現 makeSound 方法。
每一種動物都加入本身的發聲方法(makeSound)實現。AnimalSound 遍歷動物數組並調用每種動物的 makeSound 方法。
如今,若是咱們添加一種新動物,AnimalSound 不須要修改。咱們須要作的就是把新動物加入到動物數組中。
AnimalSound 方法符合 OCP 原則了。
再舉個例子。假如你有一家商店,你使用下面的類給本身最喜歡的客戶 20% 的折扣:

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}

當你決定給 VIP 客戶雙倍的折扣(40%)時,你可能會這樣修改這個類:

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

這就違反了 OCP 原則。OCP 禁止這樣作。若是想給不一樣類型的客戶一個新的折扣百分比,就得添加一段新的邏輯。
爲了使它遵循 OCP 原則,咱們將新建一個類來擴展 Discount。在這個新類中,咱們將從新實現它的行爲:

class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

若是你決定給超級 VIP 客戶 80% 的折扣,那麼代碼是下面這個樣子:

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

就是這樣,擴展而不修改。

里氏替換原則(LSP)

子類必須能夠替換它的超類。
這個原則的目的是確保子類能夠替換它的超類而沒有錯誤。若是你發現本身的代碼在檢查類的類型,那麼它必定違反了這個原則。
讓咱們以 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);

上述方法違反了 LSP 原則(也違反了 OCP 原則)。它必須知道每一種 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);

爲了使這個函數符合 LSP 原則,咱們將遵循 Steve Fenton 提出的 LSP 要求:
若是超類(Animal)有一個方法接受超類類型(Anima)的參數,那麼它的子類(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 函數並不關心傳遞的動物類型,它只管調用 LegCount 方法。它只知道參數必須是 Animal 類型,要麼是 Animal 類,要麼是它的子類。
如今,Animal 類必須實現 / 定義一個 LegCount 方法:

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

而它的子類必須實現 LegCount 方法:

//...
class Lion extends Animal{
    //...
    LegCount() {
        //...
    }
}
//...

當它被傳遞給 AnimalLegCount 函數時,它會返回一頭獅子的腿數。
如你所見,AnimalLegCount 不須要知道動物的類型就能夠返回它的腿數,它只調用了 Animal 類型的 LegCount 方法,由於根據約定,Animal 類的一個子類必須實現 LegCount 函數。

接口隔離原則(ISP)

建立特定於客戶端的細粒度接口。不該該強迫客戶端依賴於它們不使用的接口。
這個原則是爲了克服實現大接口的缺點。讓咱們看看下面的 IShape 接口:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
}

這個接口能夠繪製正方形、圓形、矩形。實現 IShape 接口的類 Circle、Square 和 Rectangle 必須定義方法 drawCircle()、drawSquare()、drawRectangle()。

class Circle implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}
class Square implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}
class Rectangle implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}

上面的代碼頗有趣。類 Rectangle 實現了它沒有使用的方法 drawCircle 和 drawSquare,一樣,Square 實現了 drawCircle 和 drawRectangle,Circle 實現了 drawSquare 和 drawRectangle。
若是咱們向 IShape 接口添加另外一個方法,好比 drawTriangle():

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
    drawTriangle();
}

那麼,這些類就必須實現新方法,不然就會拋出錯誤。
咱們看到,不可能實現這樣一種形狀類,它能夠畫圓,但不能畫矩形、正方形或三角形。咱們在實現方法時能夠只拋出一個錯誤,代表操做沒法執行。
ISP 反對 IShape 接口的這種設計。客戶端(這裏是 Rectangle、Circle 和 Square)不該該被迫依賴於它們不須要或不使用的方法。另外,ISP 指出,接口應該只執行一個任務(就像 SRP 原則同樣),任何額外的行爲都應該抽象到另外一個接口中。
在這裏,咱們的 IShape 接口執行了應該由其餘接口獨立處理的操做。爲了使 IShape 接口符合 ISP 原則,咱們將對不一樣接口的操做進行隔離:

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 處理矩形的繪製。
或者,類(Circle、Rectangle、Square、Triangle)必須繼承 IShape 接口,並實現本身的繪製行爲。

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

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

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

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

而後,咱們可使用 I- 接口建立具體的形狀,如半圓、直角三角形、等邊三角形、鈍邊矩形等。

依賴倒置原則(DIP)

依賴應該是抽象的,而不是具體的。
高級模塊不該該依賴於低級模塊。二者都應該依賴於抽象。
抽象不該該依賴於細節。細節應該依賴於抽象。
在軟件開發中,咱們的應用程序最終主要是由模塊組成。當這種狀況出現時,咱們必須使用依賴注入來解決。高級組件依賴於低級組件發揮做用。

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 是低級組件。這種設計違反了 DIP A:高級模塊不該該依賴於低級模塊。它應該依賴於它的抽象。
該 Http 類被迫依賴於 XMLHttpService 類。若是咱們要修改 Http 鏈接服務,也許咱們想經過 Nodejs 鏈接到互聯網,甚至模擬 http 服務。咱們將不得不費力地遍歷全部 Http 實例來編輯代碼,這違反了 OCP 原則。
Http 類不該該關心使用的 Http 服務的類型。咱們作了一個 Connection 接口:

interface Connection {
    request(url: string, opts:any);
}

Connection 接口有一個 request 方法。有了這個接口,咱們就能夠向 Http 類傳遞一個 Connection 類型的參數:

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 鏈接類型,並將其傳遞給 Http 類,而沒必要擔憂錯誤。

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

如今,咱們能夠看到,高級模塊和低級模塊都依賴於抽象。Http 類(高級模塊)依賴於 Connection 接口(抽象),而 Http 服務類型(低級模塊)也依賴於 Connection 接口(抽象)。
此外,DIP 原則會強制咱們遵循里氏替換原則:Connection 類型 Node-XML-MockHttpService 能夠替換它們的父類型鏈接。

小結

本文介紹了每一個軟件開發人員都必須遵照的五個原則。首先,要遵照全部這些原則可能會使人生畏,可是隨着不斷的實踐和堅持,它們會成爲咱們的一部分,並將對應用程序的維護產生巨大的影響。
關於這些原則,若是你以爲有什麼須要添加、糾正或刪除,請在下面的評論區留言,我很是樂意與你討論!
英文原文:
https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688

相關文章
相關標籤/搜索