面向物件編程(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,
結論
本文介紹了每個軟體開發人員必須遵守的五項原則,在軟體開發中,要遵守所有這些原則可能會令人心生畏懼,但是通過不斷的實踐和堅持,它將成為我們的一部分,并將對我們的應用程式維護產生巨大影響,

編譯:一點教程
https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688
歡迎關注我的公眾號::一點教程,獲得獨家整理的學習資源和日常干貨推送,
如果您對我的系列教程感興趣,也可以關注我的網站:yiidian.com
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/16286.html
標籤:架構設計
上一篇:有貨雙中心雙活架構實踐
