solid原則包括以下五個:
1、單一職責原則(SRP):表明一個類有且只有一個職責。一個類就像容器一樣,它能添加任意數量的屬性、方法等。
2、開放封閉原則(OCP):一個類應該對擴展開放,對修改關閉。這意味一旦創建了一個類並且應用程序的其他部分開始使用它,就不應該修改它。
3、里氏替換原則(LSP):派生的子類應該是可替換基類的,也就是說任何基類可以出現的地方,子類一定可以出現。值得注意的是,當通過繼承實現多態行為時,如果派生類沒有遵守LSP,可能會讓系統引發異常。
4、接口隔離原則(ISP):表明類不應該被迫依賴他們不使用的方法,也就是說一個接口應該擁有盡可能少的行為,它是精簡的,也是單一的。
5、依賴倒置原則(DIP):表明高層模塊不應該依賴低層模塊,相反,他們應該依賴抽象類或者接口。這意味着不應該在高層模塊中使用具體的低層模塊。
下面我們來分別看一下這六大設計原則。
單一職責原則(Single Responsibility Principle)
單一職責原則簡稱 SRP ,顧名思義,就是一個類只負責一個職責。它的定義也很簡單:
1、There should never be more than one reason for a class to change.
這句話很好理解,就是說,不能有多個導致類變更的原因。
那這個原則有什么用呢,它讓類的職責更單一。這樣的話,每個類只需要負責自己的那部分,類的復雜度就會降低。如果職責划分得很清楚,那么代碼維護起來也更加容易。試想如果所有的功能都放在了一個類中,那么這個類就會變得非常臃腫,而且一旦出現bug,要在所有代碼中去尋找;更改某一個地方,可能要改變整個代碼的結構,想想都非常可怕。當然一般時候,沒有人會去這么寫的。
當然,這個原則不僅僅適用於類,對於接口和方法也適用,即一個接口/方法,只負責一件事,這樣的話,接口就會變得簡單,方法中的代碼也會更少,易讀,便於維護。
事實上,由於一些其他的因素影響,類的單一職責在項目中是很難保證的。通常,接口和方法的單一職責更容易實現。
單一原則的好處:
代碼的粒度降低了,類的復雜度降低了。
可讀性提高了,每個類的職責都很明確,可讀性自然更好。
可維護性提高了,可讀性提高了,一旦出現 bug ,自然更容易找到他問題所在。
改動代碼所消耗的資源降低了,更改的風險也降低了。
里氏替換原則(Liskov Substitution Principle)
里氏替換原則的定義如下:
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻譯過來就是,所有引用基類的地方必須能透明地使用其子類的對象。
里氏替換原則的意思是,所有基類在的地方,都可以換成子類,程序還可以正常運行。這個原則是與面向對象語言的 繼承 特性密切相關的。
這是為什么呢?由於面向對象語言的繼承特性,子類擁有父類的所有方法,因此,將基類替換成具體的子類,子類也可以調用父類中的方法(其實是它自己的方法,繼承於父類),但是如果要保證完全可以調用,光名稱相同不行,還需要滿足下面兩個條件:
子類中的方法的前置條件必須與超類中被覆寫的方法的前置條件相同或更寬松。
子類中的方法的后置條件必須與超類中被覆寫的方法的后置條件相同或更嚴格。
這樣的話,調用就沒有問題了。否則,我在父類中傳入一個 List 類型的參數,子類中重寫的方法參數卻變為 ArrayList ,那客戶端使用的時候傳入一個 LinkedList 類型的參數,使用父類的時候程序正常運行,但根據 LSP 原則,替換成子類后程序就會出現問題。同理,后置條件也是如此。
是不是很好理解?
但是這個原則並不是這么簡單的,語法上是肯定不能有任何問題。但是,同時,功能上也要和替換前相同。
那么這就有些困難了,因為子類重寫父類中的方法天經地義,子類中的方法不同於父類中的方法也是很正常的,但是,如果這么隨便重寫父類中的方法的話,那么肯定會違背 LSP 原則,可以看下面的例子:
首先有一個父類,水果類:
public class Fruit {
void introduce() {
System.out.println("我是水果父類...");
}
}
其次,它有一個子類,蘋果類:
public class Apple extends Fruit {
@Override
void introduce() {
System.out.println("我是水果子類——蘋果");
}
}
客戶端代碼如下:
public static void main(String[] args) {
Fruit fruit = new Fruit();
HashMap map = new HashMap<>();
fruit.introduce();
}
運行結果:
我是水果父類...
1
那么,如果按照 LSP 原則,所有父類出現的地方都能換成其子類,代碼如下:
public static void main(String[] args) {
Apple fruit = new Apple();
HashMap map = new HashMap<>();
fruit.introduce();
}
那么運行結果就會變成:
我是水果子類——蘋果
1
與原來的輸出不同,程序的功能被改變了,違背了 LSP 原則。
因此,可以看到, LSP 原則最重要的一點就是:避免子類重寫父類中已經實現的方法。這就是 LSP 原則的本質。這里由於 Fruit 父類已經實現了 introduce 方法,因此子類應該避免再對其進行重寫,如果需要增加個性化,就應該對父類進行擴展,而不是重寫,否則也會違背開閉原則。
一般來講,程序中父類大多是抽象類,因為父類只是一個框架,具體功能還需要子類來實現。因此很少直接去 new 一個父類。而如果出現這種情況,那么就說明父類中實現的代碼已經很好了,子類只需要對其進行擴展就會,盡量避免對其已經實現的方法再去重寫。
依賴倒置原則(Dependence Inversion Principle)
依賴倒置原則的原始定義是這樣的:
High level modules should not depend upon low level modules.
Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
翻譯一下,就是下面三句話:
高層模塊不應該依賴底層模塊,兩者都應該依賴其抽象。
抽象不應該依賴細節。
細節應該依賴抽象。
在Java語言中的表現就是:
模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類產生的。
接口或抽象類不依賴於實現類。
實現類依賴於接口或抽象類。
簡而言之,我們要盡可能使用接口或抽象類。也就是“面向接口編程” 或者說 “面向抽象編程” ,也就是說程序中要盡可能使用抽象類或是接口。
可能現在還沒有使用抽象的習慣,可以看一個例子:比如我們要寫一個人吃蘋果的程序,首先創建一個蘋果類:
public class Apple {
public void eaten() {
System.out.println("正在吃蘋果...");
}
}
然后寫人的類:
public class Person {
void eat(Apple apple) {
apple.eaten();
}
}
這樣,在客戶端中我們就可以這樣調用:
Person xiaoMing = new Person();
Apple apple = new Apple();
xiaoMing.eat(apple);
但是這樣就有一個問題,如果我不僅僅想吃蘋果,還想吃橘子怎么辦,在 Person 類中再加一個函數處理橘子?那么吃別的水果呢??總不能程序中每增加一種水果就要在其中增加一個函數吧。
這時候就需要接口了(抽象類也是可以的)
程序就可以這樣更改,增加一個水果接口:
public interface Fruit {
void eaten();
}
讓蘋果類實現該接口:
public class Apple implements Fruit {
@Override
public void eaten() {
System.out.println("正在吃蘋果...");
}
}
然后將Person類中的函數的參數稍作修改:
public class Person {
void eat(Fruit fruit) {
fruit.eaten();
}
}
這樣,客戶端代碼也稍作修改:
Person xiaoMing = new Person();
Fruit apple = new Apple();
xiaoMing.eat(apple);
這樣改完之后,如果再增加一種新的水果,就不需要改變 Person 類了,方便吧。
那么再回來說我們的依賴倒置原則,依賴就是類與類之間的依賴,主要是函數傳參,上面的例子已經很明白地介紹了,參數要盡可能使用抽象類或接口,這就是“高層模塊不應該依賴底層模塊,兩者都應該依賴其抽象” 的解釋。那么如果要實現這個,就要求每個實現類都應該盡可能從抽象中派生,這就是上面的 “細節應該依賴抽象”。
簡而言之,該原則主要有下面幾點要求:
每個類都盡量要有接口或抽象類,或者兩者都有
變量的表面類型盡量是接口或者抽象類(比如程序中的 Fruit apple = new Apple() Fruit 是表面類型, Apple 是實際類型)
任何類都不應該從具體類中派生
接口隔離原則(Interface Segregation Principle)
首先聲明,該原則中的接口,是一個泛泛而言的接口,不僅僅指Java中的接口,還包括其中的抽象類。
首先,給出該原則的定義,該原則有兩個定義:
Clients should not be forced to depend upon interfaces that they don`t use.
客戶端不應該依賴它不需要的接口。
The dependency of one class to another one should depend on the smallest possible.
類間的依賴關系應該建立在最小的接口上。
這是什么意思呢,這是讓我們把接口進行細分。舉個例子,如果一個類實現一個接口,但這個接口中有它不需要的方法,那么就需要把這個接口拆分,把它需要的方法提取出來,組成一個新的接口讓這個類去實現,這就是接口隔離原則。簡而言之,就是說,接口中的所有方法對其實現的子類都是有用的。否則,就將接口繼續細分。
看起來,該原則與單一職責原則很相像。確實很像,二者都是強調要將接口進行細分,只不過分的方式不同。單一職責原則是按照 職責 進行划分接口的;而接口隔離原則則是按照實現類對方法的使用來划分的。可以說,接口隔離原則更細一些。
要想完美地實現該原則,基本上就需要每個實現類都有一個專用的接口。但實際開發中,這樣顯然是不可能的,而且,這樣很容易違背單一職責原則(可能出現同一個職責分成了好幾個接口的情況),因此我們能做的就是盡量細分。
該原則主要強調兩點:
接口盡量小。
就像前面說的那樣,接口中只有實現類中有用的方法。
接口要高內聚
就是說,在接口內部實現的方法,不管怎么改,都不會影響到接口外的其他接口或是實現類,只能影響它自己。
迪米特法則(Law of Demeter)
迪米特法則也叫最少知道原則(Least Knowledge Principle, LKP ),雖然名稱不同,但都是同一個意思:一個對象應該對其他對象有最少的了解。
該原則也很好理解,我們在寫一個類的時候,應該盡可能的少暴露自己的接口。什么意思呢?就是說,在寫類的時候,能不 public 就不 public ,所有暴露的屬性或是接口,都是不得不暴露的,這樣的話,就能保證其他類對這個類有最少的了解了。
這個原則也沒什么需要多講的,調用者只需要知道被調用者公開的方法就好了,至於它內部是怎么實現的或是有其他別的方法,調用者並不關心,調用者只關心它需要用的。反而,如果被調用者暴露太多不需要暴露的屬性或方法,那么就可能導致調用者濫用其中的方法,或是引起一些其他不必要的麻煩。
開閉原則(Open Closed Principle)
開閉原則所有設計模式原則中,最基礎的那個原則。首先,還是先來看一下它的定義:
Software entities like classes, modules and functions should be open for extension but closed for modifications.
1
翻譯過來就是:一個軟件實體,如類、模塊和函數應該對擴展開放,對修改關閉。
開閉原則之前也提到過,在 LSP 中,我們說,要避免子類重寫父類中已經實現的方法。這個時候,繼承父類就是對其進行擴展,但沒有進行修改。這就是開閉原則一個很好的體現。
那是不是開閉原則與 LSP 原則混淆了呢?並不是, LSP 原則強調的是基類與子類的關系,只是其中的一種實現方式用到了開閉原則而已。
那么開閉原則具體是什么呢?可以說,開閉原則貫穿於以上五個設計模式原則。開閉原則中的對擴展開放,就是說,如果在項目中添加一個功能的時候,可以直接對代碼進行擴展;如果要修改某一部分的功能時,我們應該做的是,盡量少做修改(完全不修改是不可能的),但是修改的時候,要保留原來的功能,只是在上面擴展出新的功能,就像版本更新一樣,更新后,依然支持舊版本。
開閉原則是一個特別重要的原則,無論是在設計模式中還是在其他領域中,這都是一個非常基礎的設計理念。
總結
總而言之,這六個設計模式原則是以后學習設計模式的基礎,它們的共同目的就是 SOLID ——建立穩定、靈活、健壯的設計。
但是原則歸原則,有時候由於種種原因,這些條條框框是不得不去打破的,一味地遵循它是不會有好果子吃的(就像接口隔離原則,不可能創建那么多的接口)。因此我們應該正確使用這些原則,主要目的還是為了我們軟件的穩定性、靈活性、健壯性和可維護性。
總算把 SOLID 原則中的五個原則說完了。但說了這么一通,好像是懂了,但是好像什么都沒記住。那么我們就來盤一盤他們之間的關系。ThoughtWorks 上有一篇文章說得挺不錯,文中說:
單一職責是所有設計原則的基礎,開閉原則是設計的終極目標。里氏替換原則強調的是子類替換父類后程序運行時的正確性,它用來幫助實現開閉原則。而接口隔離原則用來幫助實現里氏替換原則,同時它也體現了單一職責。依賴倒置原則是過程式編程與面向對象編程的分水嶺,同時它也被用來指導接口隔離原則。
簡單地說:依賴倒置原則告訴我們要面向接口編程。當我們面向接口編程之后,接口隔離原則和單一職責原則又告訴我們要注意職責的划分,不要什么東西都塞在一起。
當我們職責捋得差不多的時候,里氏替換原則告訴我們在使用繼承的時候,要注意遵守父類的約定。而上面說的這四個原則,它們的最終目標都是為了實現開閉原則。
參考:https://blog.csdn.net/houzhizhen/article/details/79993880