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

来源:https://www.cnblogs.com/yiidian/archive/2020/02/12/12297785.html
-Advertisement-
Play Games

面向對象編程(OOP)給軟體開發領域帶來了新的設計思想。很多開發人員在進行面向對象編程過程中,往往會在一個類中將具有相同目的/功能的代碼放在一起,力求以最快的方式解決當下的問題。但是,這種編程方式會導致程式代碼混亂和難以維護。因此,Robert C. Martin制定了面向對象編程的五項原則。這五個 ...


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

這五個原則被稱為SOLID原則。

S:單一職責原則

O:開閉原理

L:里氏替換原則

I:介面隔離原理

D:依賴反轉原理

我們下麵將詳細地展開來討論。

單一職責原則

單一職責原則(Single Responsibility Principle):一個類(class)只負責一件事。如果一個類承擔多個職責,那麼它就會變得耦合起來。一個職責的變更會導致另一職責的變更。

註意:該原理不僅適用於類,而且適用於軟體組件和微服務。

例如,先看看以下設計:

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 要用面向對象的方式去編程,而不要用面向過程的方式去編程 對象是各種類型的數據的集合,可以是數字、字元串、數組、函數、對象…… 對象中的內容以鍵值對方式進行存儲 對象要賦值給一個變數 var cat={ "name":"喵1", "age":4, "family":["喵爸","喵媽"], "spea ...
  • SPA單頁面應用容器 開源地址: https://github.com/yuzd/Spa 功能介紹 前端應用開發完後打包後自助上傳部署發佈 配合服務端腳本(javascript)實現服務端業務邏輯編寫渲染SSR功能 可以快速回滾到上一個版本 可以設置環境變數供SSR功能使用 服務端腳本提供執行日誌 ...
  • 最近自學到了JS運動部分,自己整理了一些js模板,望採納。 1.支持鏈式運動的模板: 先解釋一下函數中的幾個參數含義: 1)obj: 要操作的對象 2)target: 屬性要到達的目標值 3)attr: 要操作的屬性值 4)callback: 回調函數 註:調用函數時,回調函數可不寫 2.支持完美運 ...
  • 在vue、react等框架大量應用之前,我們需要使用jQuery或者原生js來操作dom寫代碼,在用原生js進行事件綁定時,我們可以應用DOM2級綁定事件的方法,即:元素.addEventListener(),因為相容性,還有: 元素.attachEvent()。所以我們需要封裝成一個方法: fun ...
  • 原文:https://zhuanlan.zhihu.com/p/23987456?refer=study-fe 大部分講 new 的文章會從面向對象的思路講起,但是我始終認為,在解釋一個事物的時候,不應該引入另一個更複雜的事物。 今天我從「省代碼」的角度來講 new。 想象我們在製作一個策略類戰爭游 ...
  • 1.HTML基礎標簽圖片常見代碼形式<img src="圖片路徑地址" alt="屬性名" title="占位符">常見的圖片格式為以下三種:.jpg(圖片有損壓縮,影響畫質)、.png(圖片無損壓縮、容積大、具有透明通道)、.gif(動圖)。圖片路徑地址分為本地圖片和網路圖片,本地圖片中分為絕對路 ...
  • 1、工廠模式介紹: 2、簡單工廠模式 3、工廠方法模式 4、小結 5、抽象工廠模式 ...
  • 需求 地區數據往往是存在強上下級關係的一種數據結構,在電商系統中是比較常應用到的,比如北京的下級地區只有海澱區、通州區……,而不會是太原市,而且在開發人員傳遞地區值的時候往往要傳遞很多的值,比如省、市、區、鎮、省Id,市id、區id、鎮id,這樣影響了代碼的美觀性及校驗強上下級關係代碼的複雜性。基於 ...
一周排行
    -Advertisement-
    Play Games
  • JWT(JSON Web Token)是一種用於在網路應用之間傳遞信息的開放標準(RFC 7519)。它使用 JSON 對象在安全可靠的方式下傳遞信息,通常用於身份驗證和信息交換。 在Web API中,JWT通常用於對用戶進行身份驗證和授權。當用戶登錄成功後,伺服器會生成一個Token並返回給客戶端 ...
  • 老周在幾個世紀前曾寫過樹莓派相關的 iOT 水文,之所以沒寫 Nano Framework 相關的內容,是因為那時候這貨還不成熟,可玩性不高。不過,這貨現在已經相對完善,老周都把它用在項目上了——第一個是自製的智能插座,這個某寶上50多塊可以買到,搜“esp32 插座”就能找到。一種是 86 型盒子 ...
  • 引言 上一篇我們創建了一個Sample.Api項目和Sample.Repository,並且帶大家熟悉了一下Moq的概念,這一章我們來實戰一下在xUnit項目使用依賴註入。 Xunit.DependencyInjection Xunit.DependencyInjection 是一個用於 xUnit ...
  • 在 Avalonia 中,樣式是定義控制項外觀的一種方式,而控制項主題則是一組樣式和資源,用於定義應用程式的整體外觀和感覺。本文將深入探討這些概念,並提供示例代碼以幫助您更好地理解它們。 樣式是什麼? 樣式是一組屬性,用於定義控制項的外觀。它們可以包括背景色、邊框、字體樣式等。在 Avalonia 中,樣 ...
  • 在處理大型Excel工作簿時,有時候我們需要在工作表中凍結窗格,這樣可以在滾動查看數據的同時保持某些行或列固定不動。凍結窗格可以幫助我們更容易地導航和理解複雜的數據集。相反,當你不需要凍結窗格時,你可能需要解凍它們以獲得完整的視野。 下麵將介紹如何使用免費.NET庫通過C#實現凍結Excel視窗以鎖 ...
  • .NET 部署 IIS 的簡單步驟一: 下載 dotnet-hosting-x.y.z-win.exe ,下載地址:.NET Downloads (Linux, macOS, and Windows) (microsoft.com) .NET 部署 IIS 的簡單步驟二: 選擇對應的版本,點擊進入詳 ...
  • 拓展閱讀 資料庫設計工具-08-概覽 資料庫設計工具-08-powerdesigner 資料庫設計工具-09-mysql workbench 資料庫設計工具-10-dbdesign 資料庫設計工具-11-dbeaver 資料庫設計工具-12-pgmodeler 資料庫設計工具-13-erdplus ...
  • 初識STL STL,(Standard Template Library),即"標準模板庫",由惠普實驗室開發,STL中提供了非常多對信息學奧賽很有用的東西。 vector vetor是STL中的一個容器,可以看作一個不定長的數組,其基本形式為: vector<數據類型> 名字; 如: vector ...
  • 前言 最近自己做了個 Falsk 小項目,在部署上伺服器的時候,發現雖然不乏相關教程,但大多都是將自己項目代碼複製出來,不講核心邏輯,不太簡潔,於是將自己部署的經驗寫成內容分享出來。 uWSGI 簡介 uWSGI: 一種實現了多種協議(包括 uwsgi、http)並能提供伺服器搭建功能的 Pytho ...
  • 1 文本Embedding 將整個文本轉化為實數向量的技術。 Embedding優點是可將離散的詞語或句子轉化為連續的向量,就可用數學方法來處理詞語或句子,捕捉到文本的語義信息,文本和文本的關係信息。 ◉ 優質的Embedding通常會讓語義相似的文本在空間中彼此接近 ◉ 優質的Embedding相 ...