原文數據庫
面向對象的編程類型爲軟件開發帶來了新的設計。編程
這使開發人員可以在一個類中組合具備相同目的/功能的數據,來實現單獨的一個功能,沒必要關心整個應用程序如何。數組
可是,這種面向對象的編程仍是會讓開發者困惑或者寫出來的程序可維護性很差。app
爲此,Robert C.Martin指定了五項指導方針。遵循這五項指導方針能讓開發人員輕鬆寫出可讀性和可維護性高的程序模塊化
這五個原則被稱爲S.O.L.I.D原則(首字母縮寫詞由Michael Feathers派生)。函數
咱們在下文會詳細討論它們微服務
筆記:本文的大多數例子可能不適合實際應用或不知足實際需求。這一切都取決於您本身的設計和用例。這都不重要,關鍵是您要了解明白這五項原則。工具
提示:SOLID原則旨在用於構建模塊化、封裝、可擴展和可組合組件的軟件。Bit是一個幫助你踐行這些原則的強大工具:它能夠幫助您在團隊中大規模地在不一樣項目中輕鬆隔離,共享和管理這些組件.來試試吧。post
Bit學習
你也能夠在這裏學習更多關於SOLID原則和Bit的東西。
「......你有一份工做」 - Loki來到雷神的Skurge:Ragnarok一個類只實現一個功能
一個類應該只負責一件事。若是一個類負責超過一件事,就會變得耦合。改功能的時候會影響另一個功能。
舉個例子,考慮這個設計:
class Animal { constructor(name: string){ } getAnimalName() { } saveAnimal(a: Animal) { } }
這個Animal類違反了SRP(單一責任原則)
怎麼違反了呢?
SRP明確說明了類只能完成一項功能,這裏,咱們把兩個功能都加上去了:animal數據管理和animal屬性管理。構造函數和getAnimalName方法管理Animal的屬性,然而,saveAnimal方法管理Animal的數據存儲。
這種設計會給之後的開發維護帶來什麼問題?
若是app的更改會影響數據庫的操做。必須會觸及並從新編譯使用Animal屬性的類以使app的更改生效。
你會發現這樣的系統缺少彈性,像多米諾骨牌同樣,更改一處會影響其餘全部的地方。
讓咱們遵循SRP原則,咱們建立了另一個用於數據操做的類:
class Animal { constructor(name: string){ } getAnimalName() { } } class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { } }
「咱們在設計類時,咱們應該把相關的功能放在一塊兒,因此當他們須要發生改變時,他們會由於一樣的緣由而改變。若是是由於不一樣的緣由須要改變它們,咱們應該嘗試把它們分開。」 - Steven Fenton
遵循這些原則讓咱們的app變得高內聚。
軟件實體(類,模塊,函數)應該是能夠擴展的,而不是修改。
繼續看咱們的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') return 'roar'; if(a[i].name == 'mouse') return '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') return 'roar'; if(a[i].name == 'mouse') return 'squeak'; if(a[i].name == 'snake') return 'hiss'; } } AnimalSound(animals);
每當新的動物加入,AnimalSound函數就須要加新的邏輯。這是個很簡單的例子。當你的app變得龐大和複雜時,你會發現每次加新動物的時候就會加一條if語句,隨後你的app和AnimalSound函數都是if語句的身影。
那怎麼修改AnimalSound函數呢?
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);
如今Animal有個makeSound的私有方法。咱們每個animal繼承了Animal類而且實現了私有方法makeSound。
每一個animal實例都會在makeSound中添加本身的實現方式。AnimalSound方法遍歷animal數組並調用其makeSound方法。
如今,若是咱們添加了新動物,AnimalSound方法不須要改變。咱們須要作的就是添加新動物到動物數組。
AnimalSound方法如今遵循了開閉原則。
另外一個例子:
假設您有一個商店,而且您使用此類給您喜好的客戶打2折:
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; } } }
哈哈哈,這樣不就背離開閉原則了麼?若是咱們又想加新的折扣,那又是一堆if語句。
爲了遵循開閉原則,咱們建立了繼承Discount的新類。在這個新類中,咱們將會實現新的行爲:
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
這個原則的目的是肯定一個子類能夠毫無錯誤地佔據其超類的位置。若是代碼會檢查本身類的類型,它必定違反了這個原則。
繼續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);
這已經違反了里氏替換(也違反了OCP原則)。它必須知道每一個Animal的類型而且調用leg-conunting相關(返回動物腿數)的方法。
若是要加入新的動物,這個方法必須通過修改才能加入。
//... 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);
來,咱們依據里氏替換改造這個方法,咱們按照Steve Fenton說的來:
如今,開始改造:
function AnimalLegCount(a: Array<Animal>) { for(let i = 0; i <= a.length; i++) { a[i].LegCount(); } } AnimalLegCount(animals);
AnimalLegCount函數更少關注傳遞的Animal類型,它只調用LegCount方法。它就只知道這參數是Animal類型,或者是其子類。
Animal類如今必須實現/定義一個LegCount方法:
class Animal { //... LegCount(); }
而後它的子類就須要實現LegCount方法:
//... class Lion extends Animal{ //... LegCount() { //... } } //...
當它傳遞給AnimalLegCount方法時,他返回獅子的腿數。
你看,AnimalLegCount不須要知道Animal的類型來返回它的腿數,它只調用Animal類型的LegCount方法,Animal類的子類必須實現LegCount函數。
制定特定客戶的細粒度接口
不該強迫客戶端依賴它不須要的接口
該原則解決實現大接口的缺點。
讓咱們看下下面這段代碼:
interface Shape { drawCircle(); drawSquare(); drawRectangle(); }
這個接口定義了畫正方形、圓形、矩形的方法。圓類、正方形類或者矩形類就必須實現 drawCircle()、 drawSquare()、drawRectangle().
class Circle implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } class Square implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } class Rectangle implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }
上面的代碼看着很可笑。矩形類實現了它不須要的方法。其餘類也一樣的。
讓咱們再加一個接口。
interface Shape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle(); }
類必須實現新方法,不然將拋出錯誤。
咱們看到不可能實現能夠繪製圓形而不是矩形或正方形或三角形的形狀。 咱們能夠實現方法來拋出一個錯誤,代表沒法執行操做。
這個Shape接口的設計不符合接口隔離原則。(此處爲Rectangle,Circle和Square)不該強制依賴於他們不須要或不使用的方法。
此外,接口隔離原則要求接口應該只執行一個動做(就像單一責任原則同樣)任何額外的行爲分組都應該被抽象到另外一個接口。
這裏,咱們的Shape接口執行應由其餘接口獨立處理的動做。
爲了使咱們的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處理矩形繪製。
依賴應該是抽象而不是concretions
高級模塊不該該依賴於低級模塊。 二者都應該取決於抽象。
抽象不該該依賴於細節。 細節應取決於抽象。
在軟件開發有一點,就是咱們的app主要由模塊組成。當發生這種狀況時,咱們必須經過使用依賴注入來清除問題。 高級組件取決於低級組件的功能。
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鏈接服務,也許咱們想經過Nodejs鏈接到互聯網,甚至模擬http服務。咱們將艱難地經過Http的全部實例來編輯代碼,這違反了OCP(依賴反轉)原則。
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(); } }
咱們能夠建立不少各類用途的Http類而且不用擔憂出問題。
class NodeHttpService implements Connection { request(url: string, opts:any) { //... } } class MockHttpService implements Connection { request(url: string, opts:any) { //... } }
如今,咱們能夠看到高級模塊和低級模塊都依賴於抽象。Http類(高級模塊)依賴Connection接口(抽象),Http服務(低級模塊)實現Connection接口。
此外,依賴反轉還強制咱們不要違反裏式替換:鏈接類型Node-XML-MockHttpService可替換其父類型Connection。
咱們涵蓋了每一個軟件開發人員必須遵照的五項原則。 一開始可能難以遵照全部這些原則,但經過長期的堅持,它將成爲咱們的一部分,並將極大地影響咱們的應用程序的維護。
若是您有任何疑問,請隨時在下面發表評論,我很樂意談談!