By Chidume Nnamdi | Oct 9, 2018
面向?qū)ο蟮木幊填愋蜑檐浖_發(fā)帶來了新的設(shè)計。
這使開發(fā)人員能夠在一個類中組合具有相同目的/功能的數(shù)據(jù),來實現(xiàn)單獨的一個功能,不必關(guān)心整個應(yīng)用程序如何。
但是,這種面向?qū)ο蟮木幊踢€是會讓開發(fā)者困惑或者寫出來的程序可維護(hù)性不好。
為此,Robert C.Martin指定了五項指導(dǎo)方針。遵循這五項指導(dǎo)方針能讓開發(fā)人員輕松寫出可讀性和可維護(hù)性高的程序
這五個原則被稱為S.O.L.I.D原則(首字母縮寫詞由Michael Feathers派生)。
- S:單一責(zé)任原則
- O:開閉原則
- L:里式替換
- I:接口隔離
- D:依賴反轉(zhuǎn)
我們在下文會詳細(xì)討論它們
筆記:本文的大多數(shù)例子可能不適合實際應(yīng)用或不滿足實際需求。這一切都取決于您自己的設(shè)計和用例。這都不重要,關(guān)鍵是您要了解明白這五項原則。
提示:SOLID原則旨在用于構(gòu)建模塊化、封裝、可擴(kuò)展和可組合組件的軟件。Bit是一個幫助你踐行這些原則的強(qiáng)大工具:它可以幫助您在團(tuán)隊中大規(guī)模地在不同項目中輕松隔離,共享和管理這些組件.來試試吧。
你也可以在這里學(xué)習(xí)更多關(guān)于SOLID原則和Bit的東西。
單一責(zé)任原則
“......你有一份工作” - Loki來到雷神的Skurge:Ragnarok
一個類只實現(xiàn)一個功能
一個類應(yīng)該只負(fù)責(zé)一件事。如果一個類負(fù)責(zé)超過一件事,就會變得耦合。改功能的時候會影響另外一個功能。
- 筆記:該原則不僅適用于類,還適用于軟件組件和微服務(wù)。
舉個例子,考慮這個設(shè)計:
class Animal {
constructor(name: string){ }
getAnimalName() { }
saveAnimal(a: Animal) { }
}
這個Animal類違反了SRP(單一責(zé)任原則)
怎么違反了呢?
SRP明確說明了類只能完成一項功能,這里,我們把兩個功能都加上去了:animal數(shù)據(jù)管理和animal屬性管理。構(gòu)造函數(shù)和getAnimalName方法管理Animal的屬性,然而,saveAnimal方法管理Animal的數(shù)據(jù)存儲。
這種設(shè)計會給以后的開發(fā)維護(hù)帶來什么問題?
如果app的更改會影響數(shù)據(jù)庫的操作。必須會觸及并重新編譯使用Animal屬性的類以使app的更改生效。
你會發(fā)現(xiàn)這樣的系統(tǒng)缺乏彈性,像多米諾骨牌一樣,更改一處會影響其他所有的地方。
讓我們遵循SRP原則,我們創(chuàng)建了另外一個用于數(shù)據(jù)操作的類:
class Animal {
constructor(name: string){ }
getAnimalName() { }
}
class AnimalDB {
getAnimal(a: Animal) { }
saveAnimal(a: Animal) { }
}
“我們在設(shè)計類時,我們應(yīng)該把相關(guān)的功能放在一起,所以當(dāng)他們需要發(fā)生改變時,他們會因為同樣的原因而改變。如果是因為不同的原因需要改變它們,我們應(yīng)該嘗試把它們分開?!?- Steven Fenton
遵循這些原則讓我們的app變得高內(nèi)聚。
開閉原則
軟件實體(類,模塊,函數(shù))應(yīng)該是可以擴(kuò)展的,而不是修改。
繼續(xù)看我們的Animal類
class Animal {
constructor(name: string){ }
getAnimalName() { }
}
我們想要遍歷動物列表并且設(shè)置它們的聲音。
//...
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函數(shù)并不符合開閉原則,因為一旦有新動物出現(xiàn),它需要修改代碼。
如果我們加一條蛇進(jìn)去,??:
//...
const animals: Array<Animal> = [
new Animal('lion'),
new Animal('mouse'),
new Animal('snake')
]
//...
我們不得不改變AnimalSound函數(shù):
//...
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);
每當(dāng)新的動物加入,AnimalSound函數(shù)就需要加新的邏輯。這是個很簡單的例子。當(dāng)你的app變得龐大和復(fù)雜時,你會發(fā)現(xiàn)每次加新動物的時候就會加一條if語句,隨后你的app和AnimalSound函數(shù)都是if語句的身影。
那怎么修改AnimalSound函數(shù)呢?
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);
現(xiàn)在Animal有個makeSound的私有方法。我們每一個animal繼承了Animal類并且實現(xiàn)了私有方法makeSound。
每個animal實例都會在makeSound中添加自己的實現(xiàn)方式。AnimalSound方法遍歷animal數(shù)組并調(diào)用其makeSound方法。
現(xiàn)在,如果我們添加了新動物,AnimalSound方法不需要改變。我們需要做的就是添加新動物到動物數(shù)組。
AnimalSound方法現(xiàn)在遵循了開閉原則。
另一個例子:
假設(shè)您有一個商店,并且您使用此類給您喜愛的客戶打2折:
class Discount {
giveDiscount() {
return this.price * 0.2
}
}
當(dāng)您決定為VIP客戶提供雙倍的20%折扣。 您可以像這樣修改類:
class Discount {
giveDiscount() {
if(this.customer == 'fav') {
return this.price * 0.2;
}
if(this.customer == 'vip') {
return this.price * 0.4;
}
}
}
哈哈哈,這樣不就背離開閉原則了么?如果我們又想加新的折扣,那又是一堆if語句。
為了遵循開閉原則,我們創(chuàng)建了繼承Discount的新類。在這個新類中,我們將會實現(xiàn)新的行為:
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
這個原則的目的是確定一個子類可以毫無錯誤地占據(jù)其超類的位置。如果代碼會檢查自己類的類型,它一定違反了這個原則。
繼續(xù)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);
這已經(jīng)違反了里氏替換(也違反了OCP原則)。它必須知道每個Animal的類型并且調(diào)用leg-conunting相關(guān)(返回動物腿數(shù))的方法。
如果要加入新的動物,這個方法必須經(jīng)過修改才能加入。
//...
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);
來,我們依據(jù)里氏替換改造這個方法,我們按照Steve Fenton說的來:
- 如果超類(Animal)有一個接受超類類型(Animal)參數(shù)的方法。 它的子類(Pigeon)應(yīng)該接受超類型(Animal類型)或子類類型(Pigeon類型)作為參數(shù)。
- 如果超類返回超類類型(Animal)。 它的子類應(yīng)該返回一個超類型(Animal類型)或子類類型(Pigeon)。
現(xiàn)在,開始改造:
function AnimalLegCount(a: Array<Animal>) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);
AnimalLegCount函數(shù)更少關(guān)注傳遞的Animal類型,它只調(diào)用LegCount方法。它就只知道這參數(shù)是Animal類型,或者是其子類。
Animal類現(xiàn)在必須實現(xiàn)/定義一個LegCount方法:
class Animal {
//...
LegCount();
}
然后它的子類就需要實現(xiàn)LegCount方法:
//...
class Lion extends Animal{
//...
LegCount() {
//...
}
}
//...
當(dāng)它傳遞給AnimalLegCount方法時,他返回獅子的腿數(shù)。
你看,AnimalLegCount不需要知道Animal的類型來返回它的腿數(shù),它只調(diào)用Animal類型的LegCount方法,Animal類的子類必須實現(xiàn)LegCount函數(shù)。
接口隔離原則
制定特定客戶的細(xì)粒度接口
不應(yīng)強(qiáng)迫客戶端依賴它不需要的接口
該原則解決實現(xiàn)大接口的缺點。
讓我們看下下面這段代碼:
interface Shape {
drawCircle();
drawSquare();
drawRectangle();
}
這個接口定義了畫正方形、圓形、矩形的方法。圓類、正方形類或者矩形類就必須實現(xiàn) drawCircle()、 drawSquare()、drawRectangle().
class Circle implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Square implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Rectangle implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
上面的代碼看著很好笑。矩形類實現(xiàn)了它不需要的方法。其他類也同樣的。
讓我們再加一個接口。
interface Shape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}
類必須實現(xiàn)新方法,否則將拋出錯誤。
我們看到不可能實現(xiàn)可以繪制圓形而不是矩形或正方形或三角形的形狀。 我們可以實現(xiàn)方法來拋出一個錯誤,表明無法執(zhí)行操作。
這個Shape接口的設(shè)計不符合接口隔離原則。(此處為Rectangle,Circle和Square)不應(yīng)強(qiáng)制依賴于他們不需要或不使用的方法。
此外,接口隔離原則要求接口應(yīng)該只執(zhí)行一個動作(就像單一責(zé)任原則一樣)任何額外的行為分組都應(yīng)該被抽象到另一個接口。
這里,我們的Shape接口執(zhí)行應(yīng)由其他接口獨立處理的動作。
為了使我們的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處理矩形繪制。
依賴反轉(zhuǎn)
依賴應(yīng)該是抽象而不是concretions
高級模塊不應(yīng)該依賴于低級模塊。 兩者都應(yīng)該取決于抽象。
抽象不應(yīng)該依賴于細(xì)節(jié)。 細(xì)節(jié)應(yīng)取決于抽象。
在軟件開發(fā)有一點,就是我們的app主要由模塊組成。當(dāng)發(fā)生這種情況時,我們必須通過使用依賴注入來清除問題。 高級組件取決于低級組件的功能。
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是低級組件。此設(shè)計違反依賴反轉(zhuǎn)第一條:高級模塊不應(yīng)該依賴于低級模塊。 兩者都應(yīng)該取決于抽象。
Http類被迫依賴于XMLHttpService類。 如果我們要改變以改變Http連接服務(wù),也許我們想通過Nodejs連接到互聯(lián)網(wǎng),甚至模擬http服務(wù)。我們將艱難地通過Http的所有實例來編輯代碼,這違反了OCP(依賴反轉(zhuǎn))原則。
Http類應(yīng)該更少關(guān)注正在使用的Http服務(wù)的類型。 我們創(chuàng)建一個Connection接口:
interface Connection {
request(url: string, opts:any);
}
Connection接口有一個請求方法。 有了這個,我們將一個Connection類型的參數(shù)傳遞給我們的Http類:
class Http {
constructor(private httpConnection: Connection) { }
get(url: string , options: any) {
this.httpConnection.request(url,'GET');
}
post() {
this.httpConnection.request(url,'POST');
}
//...
}
現(xiàn)在,Http類的無需知道它正在使用什么類型的服務(wù)。它都能正常工作。
我們現(xiàn)在可以重新寫我們的XMLHttpService類來實現(xiàn)Connection接口:
class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts:any) {
xhr.open();
xhr.send();
}
}
我們可以創(chuàng)建很多各種用途的Http類并且不用擔(dān)心出問題。
class NodeHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
class MockHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
現(xiàn)在,我們可以看到高級模塊和低級模塊都依賴于抽象。Http類(高級模塊)依賴Connection接口(抽象),Http服務(wù)(低級模塊)實現(xiàn)Connection接口。
此外,依賴反轉(zhuǎn)還強(qiáng)制我們不要違反里式替換:連接類型Node-XML-MockHttpService可替換其父類型Connection。
結(jié)論
我們涵蓋了每個軟件開發(fā)人員必須遵守的五項原則。 一開始可能難以遵守所有這些原則,但通過長期的堅持,它將成為我們的一部分,并將極大地影響我們的應(yīng)用程序的維護(hù)。
如果您有任何疑問,請隨時在下面發(fā)表評論,我很樂意談?wù)劊?/p>