SOLID原則都不知道,還敢說本身是搞開發的!

面向對象編程(OOP)給軟件開發領域帶來了新的設計思想。不少開發人員在進行面向對象編程過程當中,每每會在一個類中將具備相同目的/功能的代碼放在一塊兒,力求以最快的方式解決當下的問題。可是,這種編程方式會致使程序代碼混亂和難以維護。所以,Robert C. Martin制定了面向對象編程的五項原則。這五個原則使得開發人員能夠輕鬆建立可讀性好且易於維護的程序。java

這五個原則被稱爲SOLID原則。數據庫

S:單一職責原則編程

O:開閉原理數組

L:里氏替換原則併發

I:接口隔離原理yii

D:依賴反轉原理函數

咱們下面將詳細地展開來討論。微服務

單一職責原則

單一職責原則(Single Responsibility Principle):一個類(class)只負責一件事。若是一個類承擔多個職責,那麼它就會變得耦合起來。一個職責的變動會致使另外一職責的變動。post

注意:該原理不只適用於類,並且適用於軟件組件和微服務。學習

例如,先看看如下設計:

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

Animal類就違反了單一職責原則。

** 它爲何違反單一職責原則?**

單一職責原則指出,一個類(class)應負一個職責,在這裏,咱們能夠看到Animal類作了兩件事:Animal的數據維護和Animal的屬性管理。構造方法和getAnimalName方法是管理Animal的屬性,而saveAnimal方法負責把數據存放到數據庫。

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

若是Animal類的saveAnimal方法發生改變,那麼getAnimalName方法所在的類也須要從新編譯。這種狀況就像多米諾骨牌效果,碰到了一片骨牌會影響全部其餘骨牌。

爲了更加符合單一職責原則,咱們能夠建立了另外一個類,該類專門把Animal的數據維護方法抽取出來,以下:

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

以上的設計,讓咱們的應用程序將具備更高的內聚。

開閉原則

開閉原則(Open-Closed Principle):軟件實體(類,模塊,功能)應該對擴展開放,對修改關閉。

讓咱們繼續上動物課吧。

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

咱們想遍歷全部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不符合開閉原則,由於它不能針對新的動物關閉。

若是咱們添加新的動物,如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);

您會看到,對於每一種新動物,都會在AnimalSound函數中添加新邏輯。這是一個很是簡單的例子。當您的應用程序不斷擴展並變得複雜時,您將看到,每次在整個應用程序中添加新動物時,都會在AnimalSound函數中使用if語句一遍又一遍地重複編寫邏輯。

咱們如何使它符合開閉原則?

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方法遍歷Animal數組,並調用其makeSound方法。

如今,若是咱們添加了新動物,則無需更改AnimalSound方法。咱們須要作的就是將新動物添加到動物數組中。

如今,AnimalSound符合開閉原則。

再舉一個例子

假設你有一家商店,並使用此類向最喜歡的客戶提供20%的折扣:

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

當你決定爲VIP客戶提供雙倍的20%折扣時。您能夠這樣修改類:

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

這就違反了開閉原則啦!由於若是咱們想給不一樣客戶提供差別化的折扣時,你將要不斷地修改Discount類的代碼以添加新邏輯。

爲了遵循開閉原則,咱們將添加一個新類來繼承Discount。在這個新類中,咱們將實現新的邏輯:

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

若是你決定向超級VIP客戶提供80%的折扣,則應以下所示:

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

看吧!擴展就無需修改本來的代碼啦。

里氏替換原則

里氏替換原則(Liskov Substitution Principle):子類必須能夠替代其父類。

該原理的目的是肯定子類能夠無錯誤地佔據其父類的位置。若是代碼中發現本身正在檢查類的類型,那麼它必定違反了里氏替換原則。

讓咱們繼續使用動物示例。

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

這就違反了里氏替換原則(同時也違反了開閉原則)。由於它必須知道每種動物類型才能去調用對應的LegCount函數。

每次建立新動物時,都必須修改AnimalLegCount函數以接受新動物,以下:

//...
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函數中,只需調用統一的LegCount方法。它所關心的就是傳入的參數類型必須是Animal類型,即Animal類或其子類。

Animal類如今必須定義LegCount方法:

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

其子類必須實現LegCount方法:

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

當傳遞給AnimalLegCount函數時,它返回獅子的腿數。

你會發現,AnimalLegCount函數只管調用Animal的LegCount方法,而不須要知道Animal的具體類型便可返回其腿數。由於根據規則,Animal類的子類必須實現LegCount函數。

接口隔離原則

接口隔離原則(Interface Segregation Principle):定製客戶端的細粒度接口,不該強迫客戶端依賴於不使用的接口。該原理解決了實現大接口的缺點。

讓咱們看下面的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,drawSquare方法。

若是咱們向IShape接口添加另外一個方法,例如drawTriangle(),

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

這些類必須實現新方法,不然會編譯報錯。

接口隔離原則不同意使用以上IShape接口的設計。不該強迫客戶端(Rectangle,Circle和Square類)依賴於不須要或不使用的方法。另外,接口隔離原則也指出接口應該僅僅完成一項獨立的工做(就像單一職責原理同樣),任何額外的行爲都應該抽象到另外一個接口中。

爲了使咱們的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處理矩形的圖形。

固然,還有另外一個設計是這樣:

類(圓形,矩形,正方形,三角形等)能夠僅從IShape接口繼承並實現其本身的draw行爲,以下所示。

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

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

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

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

依賴倒置原則

依賴倒置原則(Dependency Inversion Principle):依賴應該基於抽象而不是具體。高級模塊不該依賴於低級模塊,二者都應依賴抽象。

先看下面的代碼:

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是低級組件。此設計違反了依賴倒置原則:高級模塊不該依賴於低級模塊,它應取決於其抽象。

Http類被強制依賴於XMLHttpService類。若是咱們要修改Http請求方法代碼(如:咱們想經過Node.js模擬HTTP服務)咱們將不得不修改Http類的全部方法實現,這就違反了開閉原則。

怎樣纔是更好的設計?咱們能夠建立一個Connection接口:

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

該Connection接口具備請求方法。這樣,咱們將類型的參數傳遞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類的哪一個方法,它均可以輕鬆發出請求,而無需理會底層究竟是什麼樣實現代碼。

咱們能夠從新設計XMLHttpService類,讓其實現Connection接口:

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

以此類推,咱們能夠建立許多Connection類型的實現類,並將其傳遞給Http類。

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

如今,咱們能夠看到高級模塊和低級模塊都依賴於抽象。Http類(高級模塊)依賴於Connection接口(抽象),而XMLHttpService類、MockHttpService 、或NodeHttpService類 (低級模塊)也是依賴於Connection接口(抽象)。

與此同時,依賴倒置原則也迫使咱們不違反里氏替換原則:上面的實現類Node- XML- MockHttpService能夠替代他們的父類型Connection。

結論

本文介紹了每一個軟件開發人員必須遵照的五項原則。在軟件開發中,要遵照全部這些原則可能會使人心生畏懼,可是經過不斷的實踐和堅持,它將成爲咱們的一部分,並將對咱們的應用程序維護產生巨大影響。
file

編譯:一點教程

https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688

歡迎關注個人公衆號::一點教程。得到獨家整理的學習資源和平常乾貨推送。
若是您對個人系列教程感興趣,也能夠關注個人網站:yiidian.com

相關文章
相關標籤/搜索