標籤:關於 構建 子類 應用程式 英文 輕鬆 srp als 構造
- 每個開發人員都應該知道的SOLID原則
- 單一職責原則(SRP)
- 它為什麼違反了 SRP?
- 這種設計將來會帶來什麼問題?
- 開閉原則(OCP)
- 裡氏替換原則(LSP)
- 介面隔離原則(ISP)
- 依賴倒置原則(DIP)
- 小結
每個開發人員都應該知道的SOLID原則
轉載自此,原作者為 Chidume Nnamdi。
物件導向編程帶來了新的軟體開發設計方法。它使得開發人員能夠將具有相同作用 / 功能的資料群組合到一個類中,實現唯一的目的,而不管整個應用程式如何。
但是,這種物件導向的編程並不能防止難以理解或不可維護的程式。因此,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 Fenton
恰當運用這條原則,我們的應用程式就會變成高內聚的。
開閉原則(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
每個開發人員都應該知道的SOLID原則