每位開發(fā)者都應(yīng)該知道SOLID原則

By Chidume Nnamdi | Oct 9, 2018

原文

面向?qū)ο蟮木幊填愋蜑檐浖_發(fā)帶來了新的設(shè)計。

這使開發(fā)人員能夠在一個類中組合具有相同目的/功能的數(shù)據(jù),來實現(xiàn)單獨的一個功能,不必關(guān)心整個應(yīng)用程序如何。

但是,這種面向?qū)ο蟮木幊踢€是會讓開發(fā)者困惑或者寫出來的程序可維護(hù)性不好。

為此,Robert C.Martin指定了五項指導(dǎo)方針。遵循這五項指導(dǎo)方針能讓開發(fā)人員輕松寫出可讀性和可維護(hù)性高的程序

這五個原則被稱為S.O.L.I.D原則(首字母縮寫詞由Michael Feathers派生)。

  • S:單一責(zé)任原則
  • O:開閉原則
  • L:里式替換
  • I:接口隔離
  • D:依賴反轉(zhuǎn)

我們在下文會詳細(xì)討論它們

筆記:本文的大多數(shù)例子可能不適合實際應(yīng)用或不滿足實際需求。這一切都取決于您自己的設(shè)計和用例。這都不重要,關(guān)鍵是您要了解明白這五項原則。

提示:SOLID原則旨在用于構(gòu)建模塊化、封裝、可擴(kuò)展和可組合組件的軟件。Bit是一個幫助你踐行這些原則的強(qiáng)大工具:它可以幫助您在團(tuán)隊中大規(guī)模地在不同項目中輕松隔離,共享和管理這些組件.來試試吧。

Bit

你也可以在這里學(xué)習(xí)更多關(guān)于SOLID原則和Bit的東西。

單一責(zé)任原則

“......你有一份工作” - Loki來到雷神的Skurge:Ragnarok

一個類只實現(xiàn)一個功能

一個類應(yīng)該只負(fù)責(zé)一件事。如果一個類負(fù)責(zé)超過一件事,就會變得耦合。改功能的時候會影響另外一個功能。

  • 筆記:該原則不僅適用于類,還適用于軟件組件和微服務(wù)。

舉個例子,考慮這個設(shè)計:

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

這個Animal類違反了SRP(單一責(zé)任原則)

怎么違反了呢?

SRP明確說明了類只能完成一項功能,這里,我們把兩個功能都加上去了:animal數(shù)據(jù)管理和animal屬性管理。構(gòu)造函數(shù)和getAnimalName方法管理Animal的屬性,然而,saveAnimal方法管理Animal的數(shù)據(jù)存儲。

這種設(shè)計會給以后的開發(fā)維護(hù)帶來什么問題?

如果app的更改會影響數(shù)據(jù)庫的操作。必須會觸及并重新編譯使用Animal屬性的類以使app的更改生效。

你會發(fā)現(xiàn)這樣的系統(tǒng)缺乏彈性,像多米諾骨牌一樣,更改一處會影響其他所有的地方。

讓我們遵循SRP原則,我們創(chuàng)建了另外一個用于數(shù)據(jù)操作的類:

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

“我們在設(shè)計類時,我們應(yīng)該把相關(guān)的功能放在一起,所以當(dāng)他們需要發(fā)生改變時,他們會因為同樣的原因而改變。如果是因為不同的原因需要改變它們,我們應(yīng)該嘗試把它們分開?!?- Steven Fenton

遵循這些原則讓我們的app變得高內(nèi)聚。

開閉原則

軟件實體(類,模塊,函數(shù))應(yīng)該是可以擴(kuò)展的,而不是修改。

繼續(xù)看我們的Animal類

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

我們想要遍歷動物列表并且設(shè)置它們的聲音。

//...
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')
            return 'roar';
        if(a[i].name == 'mouse')
            return 'squeak';
    }
}
AnimalSound(animals);

AnimalSound函數(shù)并不符合開閉原則,因為一旦有新動物出現(xiàn),它需要修改代碼。

如果我們加一條蛇進(jìn)去,??:

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

我們不得不改變AnimalSound函數(shù):

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

每當(dāng)新的動物加入,AnimalSound函數(shù)就需要加新的邏輯。這是個很簡單的例子。當(dāng)你的app變得龐大和復(fù)雜時,你會發(fā)現(xiàn)每次加新動物的時候就會加一條if語句,隨后你的app和AnimalSound函數(shù)都是if語句的身影。

那怎么修改AnimalSound函數(shù)呢?

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++) {
        a[i].makeSound();
    }
}
AnimalSound(animals);

現(xiàn)在Animal有個makeSound的私有方法。我們每一個animal繼承了Animal類并且實現(xiàn)了私有方法makeSound。

每個animal實例都會在makeSound中添加自己的實現(xiàn)方式。AnimalSound方法遍歷animal數(shù)組并調(diào)用其makeSound方法。

現(xiàn)在,如果我們添加了新動物,AnimalSound方法不需要改變。我們需要做的就是添加新動物到動物數(shù)組。

AnimalSound方法現(xiàn)在遵循了開閉原則。

另一個例子:

假設(shè)您有一個商店,并且您使用此類給您喜愛的客戶打2折:

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

當(dāng)您決定為VIP客戶提供雙倍的20%折扣。 您可以像這樣修改類:

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

哈哈哈,這樣不就背離開閉原則了么?如果我們又想加新的折扣,那又是一堆if語句。

為了遵循開閉原則,我們創(chuàng)建了繼承Discount的新類。在這個新類中,我們將會實現(xiàn)新的行為:

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

如果你決定給VIP80%的折扣,就像這樣:

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

你看,這不就不用改了。

里氏替換

A sub-class must be substitutable for its super-class

這個原則的目的是確定一個子類可以毫無錯誤地占據(jù)其超類的位置。如果代碼會檢查自己類的類型,它一定違反了這個原則。

繼續(xù)Animal例子。

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

這已經(jīng)違反了里氏替換(也違反了OCP原則)。它必須知道每個Animal的類型并且調(diào)用leg-conunting相關(guān)(返回動物腿數(shù))的方法。

如果要加入新的動物,這個方法必須經(jīng)過修改才能加入。

//...
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)
            return LionLegCount(a[i]);
        if(typeof a[i] == Mouse)
            return MouseLegCount(a[i]);
         if(typeof a[i] == Snake)
            return SnakeLegCount(a[i]);
        if(typeof a[i] == Pigeon)
            return PigeonLegCount(a[i]);
    }
}
AnimalLegCount(animals);

來,我們依據(jù)里氏替換改造這個方法,我們按照Steve Fenton說的來:

  • 如果超類(Animal)有一個接受超類類型(Animal)參數(shù)的方法。 它的子類(Pigeon)應(yīng)該接受超類型(Animal類型)或子類類型(Pigeon類型)作為參數(shù)。
  • 如果超類返回超類類型(Animal)。 它的子類應(yīng)該返回一個超類型(Animal類型)或子類類型(Pigeon)。

現(xiàn)在,開始改造:

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

AnimalLegCount函數(shù)更少關(guān)注傳遞的Animal類型,它只調(diào)用LegCount方法。它就只知道這參數(shù)是Animal類型,或者是其子類。

Animal類現(xiàn)在必須實現(xiàn)/定義一個LegCount方法:

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

然后它的子類就需要實現(xiàn)LegCount方法:

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

當(dāng)它傳遞給AnimalLegCount方法時,他返回獅子的腿數(shù)。

你看,AnimalLegCount不需要知道Animal的類型來返回它的腿數(shù),它只調(diào)用Animal類型的LegCount方法,Animal類的子類必須實現(xiàn)LegCount函數(shù)。

接口隔離原則

制定特定客戶的細(xì)粒度接口
不應(yīng)強(qiáng)迫客戶端依賴它不需要的接口

該原則解決實現(xiàn)大接口的缺點。

讓我們看下下面這段代碼:

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

這個接口定義了畫正方形、圓形、矩形的方法。圓類、正方形類或者矩形類就必須實現(xiàn) drawCircle()、 drawSquare()、drawRectangle().

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

上面的代碼看著很好笑。矩形類實現(xiàn)了它不需要的方法。其他類也同樣的。

讓我們再加一個接口。

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

類必須實現(xiàn)新方法,否則將拋出錯誤。

我們看到不可能實現(xiàn)可以繪制圓形而不是矩形或正方形或三角形的形狀。 我們可以實現(xiàn)方法來拋出一個錯誤,表明無法執(zhí)行操作。

這個Shape接口的設(shè)計不符合接口隔離原則。(此處為Rectangle,Circle和Square)不應(yīng)強(qiáng)制依賴于他們不需要或不使用的方法。

此外,接口隔離原則要求接口應(yīng)該只執(zhí)行一個動作(就像單一責(zé)任原則一樣)任何額外的行為分組都應(yīng)該被抽象到另一個接口。

這里,我們的Shape接口執(zhí)行應(yīng)由其他接口獨立處理的動作。

為了使我們的Shape接口符合ISP原則,我們將操作分離到不同的接口:

interface Shape {
    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 Shape {
   draw(){
      //...
   }
}

ICircle接口僅處理圓形繪畫,Shape處理任何形狀的繪圖:),ISquare處理僅正方形的繪制和IRectangle處理矩形繪制。

依賴反轉(zhuǎn)

依賴應(yīng)該是抽象而不是concretions
高級模塊不應(yīng)該依賴于低級模塊。 兩者都應(yīng)該取決于抽象。
抽象不應(yīng)該依賴于細(xì)節(jié)。 細(xì)節(jié)應(yīng)取決于抽象。

在軟件開發(fā)有一點,就是我們的app主要由模塊組成。當(dāng)發(fā)生這種情況時,我們必須通過使用依賴注入來清除問題。 高級組件取決于低級組件的功能。

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是低級組件。此設(shè)計違反依賴反轉(zhuǎn)第一條:高級模塊不應(yīng)該依賴于低級模塊。 兩者都應(yīng)該取決于抽象。

Http類被迫依賴于XMLHttpService類。 如果我們要改變以改變Http連接服務(wù),也許我們想通過Nodejs連接到互聯(lián)網(wǎng),甚至模擬http服務(wù)。我們將艱難地通過Http的所有實例來編輯代碼,這違反了OCP(依賴反轉(zhuǎn))原則。

Http類應(yīng)該更少關(guān)注正在使用的Http服務(wù)的類型。 我們創(chuàng)建一個Connection接口:

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

Connection接口有一個請求方法。 有了這個,我們將一個Connection類型的參數(shù)傳遞給我們的Http類:

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

現(xiàn)在,Http類的無需知道它正在使用什么類型的服務(wù)。它都能正常工作。

我們現(xiàn)在可以重新寫我們的XMLHttpService類來實現(xiàn)Connection接口:

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

我們可以創(chuàng)建很多各種用途的Http類并且不用擔(dān)心出問題。

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

現(xiàn)在,我們可以看到高級模塊和低級模塊都依賴于抽象。Http類(高級模塊)依賴Connection接口(抽象),Http服務(wù)(低級模塊)實現(xiàn)Connection接口。

此外,依賴反轉(zhuǎn)還強(qiáng)制我們不要違反里式替換:連接類型Node-XML-MockHttpService可替換其父類型Connection。

結(jié)論

我們涵蓋了每個軟件開發(fā)人員必須遵守的五項原則。 一開始可能難以遵守所有這些原則,但通過長期的堅持,它將成為我們的一部分,并將極大地影響我們的應(yīng)用程序的維護(hù)。

如果您有任何疑問,請隨時在下面發(fā)表評論,我很樂意談?wù)劊?/p>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄: 設(shè)計模式六大原則(1):單一職責(zé)原則 設(shè)計模式六大原則(2):里氏替換原則 設(shè)計模式六大原則(3):依賴倒...
    加油小杜閱讀 798評論 0 1
  • 設(shè)計模式六大原則 設(shè)計模式六大原則(1):單一職責(zé)原則 定義:不要存在多于一個導(dǎo)致類變更的原因。通俗的說,即一個類...
    viva158閱讀 826評論 0 1
  • 今天看到一句話,瞬間被秒到:所謂成功,就是有時間陪自己的小孩。出自《窮爸爸富爸爸》一書。這也讓我開始反思自己從原來...
    溯源而上閱讀 203評論 0 0
  • 上午主題課是“如何打斷別人說話”,小朋友的參與度還是挺高的,中午在臥室陪小朋友,阿西婭因我阻止了她的行為而不高興,...
    Carsyn_aa55閱讀 196評論 1 0
  • Roader閱讀 323評論 0 0

友情鏈接更多精彩內(nèi)容