名稱 | 英文名稱 | 解釋 |
---|---|---|
單一職責原則 | Single Responsibility Principle | 一個類只負責一項職責 |
里氏替換原則 | Liskov Substitution Principle | 父類能出現的地方都可以替換為子類,反之則不一定 |
依賴倒置原則 | Dependence Inversion Principle | 抽象不應該依賴于細節,細節應該依賴于抽象 |
接口隔離原則 | Interface Segregation Principle | 客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。 |
迪米特法則 | Law of Demeter | 一個對象應該對其他對象保持最少的了解。降低類與類之間的耦合(高內聚,低耦合) |
開閉原則 | Open Closed Principle | 軟件實體應當對擴展開放,對修改關閉,即軟件實體應當在不修改的前提下擴展。 |
單一職責原則
一個類為什么要只負責一項職責呢?因為如果一個類承擔的職責過多,就等于把這些職責耦合在一起,當其中一個職責發生變化時,會造成意想不到的破壞。單一職責并不是說一個類只有一個函數,而是說這個類中的函數都是高度相關的,也就是高內聚。單一職責原則關注的是職責,最難的也是職責的劃分,上圖中的發郵件和發短信其實放到一起也是可以的,這兩個行為是否相關的界定是需要根據項際目的實情況來判斷的。
里氏替換原則
父類能出現的地方都可以替換為子類,換句話說,子類可以擴展父類功能,但不能改變父類原有功能。
public class Father {
public void sendEmail(){
//發送了郵件
}
}
public class Son extends Father{
@Override
public void sendEmail(){
//發送了短信
}
}
上面這個例子中,父類定義了發郵件的方法,但是子類卻發了短信,父類和子類的行為不一致,明顯違背了里氏替換原則。父類的sendEmail方法設定了規范,子類不能任意修改。
依賴倒置原則
依賴倒置原則的解釋是抽象不應該依賴細節,細節應該依賴于抽象,通俗的說,就是面向接口編程而不是面向實現。舉個小明開槍射擊的例子。
//手槍
class Pistol{
public void fire(){
//單發射擊
}
}
//小明
class Xiaoming{
public void shoot(Pistol pistol){
pistol.fire();
}
}
//場景類
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
xiaoming.shoot(new Pistol());
}
}
小明用手槍射擊沒有問題,但是有一天,需求改成讓小明用步槍射擊怎么辦?
//增加一個步槍類
class Rifle{
public void fire(){
//連發射擊
}
}
//修改小明的射擊方法
class Xiaoming{
public void shoot(Rifle rifle){
rifle.fire();
}
}
//修改場景類
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
xiaoming.shoot(new Rifle());
}
}
因為手槍和小明是強耦合關系,所以換成步槍必須修改小明類和場景類才行,以后換成其他槍,還是要不斷修改,這顯然不是個好設計。如何降低手槍和小明的耦合呢?添加一個抽象接口不就行了嘛!改造后如下:
//抽象一個槍的接口
interface IGUN{
void fire();
}
//手槍
class Pistol implements IGUN{
public void fire(){
//單發射擊
}
}
//步槍類
class Rifle implements IGUN{
public void fire(){
//連發射擊
}
}
//小明
class Xiaoming{
public void shoot(IGUN gun){
rifle.fire();
}
}
//場景類
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
//用手槍射擊
xiaoming.shoot(new Pistol());
//用步槍射擊
xiaoming.shoot(new Rifle());
}
}
這樣是不是就好多了,以后需求再讓小明用別的槍,就不用那么麻煩了。
接口隔離原則
接口隔離原則是說要避免臃腫的接口,盡量細化接口,其實也是為了解耦。舉例說明一下:
首先,定義一個功能聚合的(臃腫的)接口
//各種功能定義到一個接口中
interface IFuns{
void playAudio(); //播放音頻
void playVideo(); //播放視頻
void readBook(); //閱讀書籍
void readComic(); //瀏覽漫畫
//...
}
現在要生產兩個產品,一個是mp4,一個是kindle,代碼如下
//mp4目前只能播放音頻和視頻
class Mp4 implements IFuns{
public void fire(){
public void playAudio(){
//播放音頻
};
public void playVideo(){
//播放視頻
};
public void readBook(){
//不支持
};
public void readComic(){
//不支持
};
}
}
//kindle的設計只是為了閱讀書籍和瀏覽漫畫
class Kindle implements IFuns{
public void fire(){
public void playAudio(){
//不支持
};
public void playVideo(){
//不支持
};
public void readBook(){
//閱讀書籍
};
public void readComic(){
//瀏覽漫畫
};
}
}
要實例化產品出來,每個產品必須實現IFuns中所有的功能,但是呢,對于mp4,目前只研發出了看視頻和音頻的功能,其他功能還在研發中,對于kindle,這個產品是專注于閱讀書籍和瀏覽漫畫的,不想具備其他功能?,F在的設計要求必須實現產品不需要的功能,這是不滿足需求的。于是IFuns接口被拆分為:
interface IFunsA{
void playAudio(); //播放音頻
void playVideo(); //播放視頻
}
interface IFunsB{
void readBook(); //閱讀書籍
void readComic(); //瀏覽漫畫
}
//Mp4實現IFunsA;Kindle實現IFunsB 代碼省略
這樣拆分滿足了kindle專注于閱讀書籍和瀏覽漫畫而不想具備其他功能。但是呢,對于Mp4來說,并不是不想具備其他功能,只是現在還沒研發出來而已,以后mp4還是需要實現閱讀書籍和瀏覽漫畫的。于是還需要進行拆分:
interface IFunsA{
void playAudio(); //播放音頻
}
interface IFunsB{
void playVideo(); //播放視頻
}
interface IFunsC{
void readBook(); //閱讀書籍
}
interface IFunsD{
void readComic(); //瀏覽漫畫
}
//Mp4實現IFunsA,IFunsB;Kindle實現IFunsC,IFunsD 代碼省略
這樣拆分后,產品需要什么功能,實現對應的接口就行了。
這個結果看上去很像單一職責原則?。侩m然看起來差不來,其實是不一樣的。單一職責原則注重的是職責,與接口中的方法數量無關,而接口隔離原則注重于對接口的解耦,接口隔離是在滿足單一職責的基礎上,盡可能減少方法。
迪米特法則
又稱為最少知道原則。定義是一個對象應當對其他對象有盡可能少的了解,不和陌生人說話。通俗的說就是一個類對于它需要調用的類,知道的越少越好,如果知道的越多,類之間的關系就會越密切,耦合度就會變得越來越高,耦合度高了,類的復用率就降低了。
有篇文章對迪米特法則解釋很好,可以點進去看看
開閉原則
軟件實體應當在不修改的前提下擴展,就是說,需求變化時,不要修改已有的穩定的代碼,可以通過擴展的方式滿足需求,以避免引入新的bug。
這個原則感覺沒什么好解釋的了,因為前面5個原則遵守好了,設計出來的軟件就是符合開閉原則的。換句話說,開閉原則是其他5個原則的抽象。
軟件設計最大的難題就是應對需求的不斷變化,開閉原則應該說是軟件設計的最終目標了。