原文:SOLID Principles every Developer Should Know – Bits and Piecesjavascript
面向對象爲軟件開發帶來了新的設計方式,它使得開發者能夠將具備相同目的或功能的數據結組合到一個類中來完成單一的目的,不須要考慮整個應用。java
可是,面向對象編程沒有減小混亂和不可維護的程序。正是這樣,Robert C. Martin發展出了5條指南/準則,讓開發者能夠易於建立可讀且易於維護的程序。數據庫
這5條準則就是S.O.L.I.D原則(縮寫是Michael Feathers推演出來的)編程
接下來咱們詳細討論上述原則。
注意: 本文的大部分例子可能不能知足或者適用現實世界的應用程序。要視你本身的實際設計和使用場景來定。最重要的是理解和掌握如何運用或遵循這些原則。數組
建議:使用Bit這樣的工具來實踐SOLID原則,它能幫助你組織,發現和重用構建新應用程序的組件。組件能夠在不一樣項目之間被發現和共享,因此你能夠更快地構建應用程序,不妨試試。網絡
「...You had one job」---Loki to Skurge in Thor: Ragnarok函數
一個類只負責一件事。若是一個類有多項責任,它就變耦合了。一個功能的變更會形成另一個功能改變。微服務
例如,考慮這樣一個設計:工具
class Animal{ constructor(name: string){} getAnimalName(){} saveAniamal(a: Animal){} }
這裏的Animal類是否違背了單一功能原則(SRP)?post
怎樣違背的?
SRP中說一個類應只含一個功能,如今咱們能分出兩個功能:動物數據管理和動物特性管理。構造函數和getAnimalName管理動物特性,而saveAnimal負責動物在數據庫中的存儲。
這個設計未來會引起怎樣的問題?
那部分若是應用程序對數據庫管理相關函數做變動,使用了動物特性功能的代碼也要會受影響而且要從新編譯來適應新的變動。
可見這個系統顯得很死板,好像一個多米諾骨牌效應,觸動一張牌就會影響排列中的全部其餘牌。
爲了符合SRP,咱們建立另外一個單一功能的類只負責將一個動物存儲到一個數據庫中:
class Animal { constuctor(name: string) { } getAnimalName() { } } class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { } }
When designing our classes, we should aim to put related features together, so whenever they tend to change they change for the same reason. And we should try to separate features if they will change for different reasons. 咱們在設計類的時候,要以將相關的特性放在一塊兒爲目標, 當他們須要改變時應當是出於相同的緣由, 若是咱們發現他們會由於不一樣的緣由改變,則需考慮將特性拆分開來 ---Steve Fenton
Software entities(Classes, modules, functions) should be open for extension, not modification.
軟件實體(類,模塊,函數等)應當對擴展開放,而對變動是封閉的
繼續討論Animal類,
class Animal { construtor(name: string) { } getAnimalName() { } }
咱們想遍歷一個animal列表而且讓發出他們的聲音。
//... const animals: Array<Animals> = [ 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') ]; //...
咱們不得不修改AnimalSoound函數
//... 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語句將在程序中不斷地重複出現。
那怎樣使它符合開閉原則(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添加發聲音的實現,遍歷動物數組的時候只須要調用它們的makeSound方法。
這樣,若是有新動物要添加,AnimalSound不要改變。咱們只須要向動物數組中添加新的動物。
再舉一例:
假設你有一家商店,你但願給你最喜好的那些顧客20%的優惠,下面是類實現:
class Discount { giveDiscount() { return this.price * 0.2; } }
當你決定給VIP用戶的折扣翻倍,你可能會這樣修改類:
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; } }
這樣,不需修改就實現了擴展。
A sub-class must be substitutable for its super class
子類必定能用父級類替換
這條原則就是目的就是確保子類能無差錯地代替父類的位置。若是代碼發現它還須要檢查子類的類型,那麼它就不符合這條原則。
用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。它必須肯定每種動物的類型並調用相應的計腿方法。
每當新增一種動物,這個函數都須要作出修改來適應。
//... 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])); } } AimalLegCount(animals);
要使這個函數符合LSP,須要遵循Steven Fenton 提出的如下要求:
如今來從新實現AnimalLegCount函數:
function AnimalLegCount(a: Array<Animal>) { for(let i = 0; i <= a.length; i++) { log(a[i].LegCount()); } } AnimalLegCount(animals);
AnimalLegCount函數如今更少關心傳遞的Animal的類型,它只是調用LegCount方法。它只知道傳入的參數必須是Animal類型,不管是Animal類型仍是他的子類。
Animal類型如今須要實現/定義一個LegCount方法:
class Animal { //... LegCount(); }
它的子類也須要實現LegCount方法:
class Lion extends Animal{ //... LegCount() { //... } }
當它被傳遞給AnimalLegCount函數時,他將返回一頭獅子的腿數。
可見AnimalLegCount函數不須要知道Animal的具體類型,只須要調用Animal類的LegCount方法,由於按約定Animal類的子類都必須實現LegCount函數。
Make fine grained interfaces that are client specific
爲特定客戶製做細粒度的接口
Clients should not be forced to depend upon interfacees that they do not use
客戶應當不會被迫以來他們不會使用的接口
這條原則用於處理實現大型接口時的弊端。來看以下接口IShape:
interface Ishape { drawCircle(); drawSquare(); drawRectangle(); }
這個接口能夠畫圓形,方形,矩形。Circle類,Square類,Rectangel類實現IShape接口的時候必須定義drawCircle(),drawSqure(),drawRectangle()方法。
class Circle implements Ishape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } class Square implements Ishape { drawCircle() { //... } drawSquare(){ //... } drawRectangle(){ //... } } class Rectangel implements Ishape { drawCircle() { //... } drawSquare(){ //... } drawRectangle(){ //... } }
上面的代碼看起來就很怪。Rectangle類藥實現它用不上的drawCircle(),drawSquare()方法,Square類和Circle類也同理。
若是咱們向Ishape中增長一個接口,如drawTriangle():
interface IShape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle(); }
全部子類都須要實現這個新方法,不然就會報錯。
也能看出不可能實現一個能夠畫圓可是不能畫方,或畫矩形及三角形的圖形類。咱們能夠只是爲上述子類都實現全部方法可是拋出錯誤指明不正確的操做不能被執行。
ISP不提倡IShape的上述實現。客戶(這裏的Circle, Rectangle, Square, Triangle)不該被強迫依賴於它們不須要或用不上的方法。ISP還指出一個接口只作一件事(與SRP相似),全部其餘分組的行爲都應當被抽象到其餘的接口中。
這裏, Ishape接口執行了本應由其餘接口獨立處理的行爲。
爲了使IShape符合ISP原則,咱們將這些行爲分離到不一樣的接口中去:
interface Ishape { draw(); } interface ICircle { drawCircle(); } interface ISquare { drawSquare(); } interface IRecetangle { 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() { //... } }
我如今還可使用I-接口來建立更多特殊形狀,如Semi
circle, Right-Angled Triangle, Equilateral Triangle, Blunt-Edged Rectangle等等。
Dependency should be on abstractions not concretion
依賴於抽象而非具體實例
A. High-level modules should not depend upon low-level modules. Both should depend upon avstractions.
B. Abstractions should not depend on deatils. Details should depend upon abstractions.
A. 上層模塊不該該依賴於下層模塊。它們都應該依賴於抽象。
B. 抽象不該該依賴於細節。細節應該依賴於抽象。
這對開發由許多模塊構成的應用程序十分重要。這時候,咱們必須使用依賴注入(dependency injection) 來理清關係、上層元件依賴於下層元件來工做。
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方法。咱們經過他傳遞一個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鏈接服務傳遞過來,Http類均可以輕鬆的鏈接到網絡,無需關心網絡鏈接的類型。
如今咱們能夠從新實現XMLHttpService類來實現Connection 接口:
class XMLHttpService implements Connection { const xhr = new XMLHttpRequest(); //... request(url: string, opts: any) { xhr.open(); xhr.send(); } }
咱們能夠建立許多的Http Connection類型而後傳遞給Http類但不會引起任何錯誤。
class NodeHttpService implements Connection { request(url: string, opts: any){ //... } } class MockHttpService implements Connection { request(url: string, opts:any) { //... } }
如今,能夠看到上層模塊和下層模塊都依賴於抽象。 Http類(上層模塊)依賴於Connection接口(抽象),並且Http服務類型(下層模塊)也依賴於Connection接口(抽象)。
咱們討論了每一個軟件開發者都須要聽從的五大原則。剛開始的時候要遵照這些原則可能會有點難,可是經過持續的練習和堅持,它將成爲咱們的一部分而且對維護咱們的應用程序產生巨大的影響。
若是您有任何疑問或者有認爲須要增長,更正或者移除的內容,儘管在下方留言,我會樂意與您討論!
原文:SOLID Principles every Developer Should Know – Bits and Pieces