單一職責原則(SRP),就一個類而言,應該僅有一個引發它變化的緣由。前端
開放-封閉原則,是說軟件實體(類、模塊、函數等待)應該能夠擴展,可是不可修改。編程
里氏代換原則(LSP):子類型必須可以替換掉它的父類型。設計模式
依賴倒轉原則:A. 高層模塊不該該依賴底層模塊,兩個都應該依賴抽象;B. 抽象不該該依賴細節,細節應該依賴抽象。函數
迪米特法則(LoD):若是兩個類沒必要彼此直接通訊,那麼這兩個類就不該當發生直接的相互做用。若是其中一個類須要調用另外一個類的某一個方法的話,能夠經過第三者轉發這個調用。學習
下面是大神總結的,寫的很好,好理解!測試
設計模式:面向對象設計的六大原則 (絕對詳細)
目錄優化
前言
六大原則
單一職責原則
開閉原則
里氏替換原則
依賴倒置原則
接口隔離原則
迪米特原則
總結
前言
好久沒有寫博客了,一直給本身找藉口說太忙了,過幾天有空再寫,幾天以後又幾天,時間就這麼快速的消逝。說到底就是本身太懶了,不下點決心真是不行。我決定逼本身一把,從今天開始學習設計模式系列,並寫成博文記錄下來,作不到的話,就罰本身一個月不玩遊戲 (做孽啊。。。。)this
六大原則
言歸正傳,這是我學習設計模式系列的第一篇文章,本文主要講的是面向對象設計應該遵循的六大原則,掌握這些原則能幫助咱們更好的理解面向對象的概念,也能更好的理解設計模式。這六大原則分別是:編碼
單一職責原則——SRP
開閉原則——OCP
裏式替換原則——LSP
依賴倒置原則——DIP
接口隔離原則——ISP
迪米特原則——LOD
單一職責原則
單一職責原則,Single Responsibility Principle,簡稱SRP。其定義是應該有且僅有一個類引發類的變動,這話的意思就是一個類只擔負一個職責。spa
舉個例子,在創業公司裏,因爲人力成本控制和流程不夠規範的緣由,每每一我的須要擔任N個職責,一個工程師可能不只要出需求,還要寫代碼,甚至要面談客戶,光背的鍋就好幾種,簡單用代碼表達大概如此:
public class Engineer {
public void makeDemand(){}
public void writeCode(){}
public void meetClient(){}
}
代碼看上去好像沒什麼問題,由於咱們平時就是這麼寫的啊,可是細讀一下就能發現,這種寫法很明顯不符合單一職責的原則,由於引發類的變化不僅有一個,至少有三個方法均可以引發類的變化,好比有天由於業務須要,出需求的方法須要加個功能 (好比需求的成本分析),或者是見客戶也須要個參數之類的,那樣一來類的變化就會有多種可能性了,其餘引用該類的類也須要相應的變化,若是引用類的數目不少的話,代碼維護的成本可想而知會有多高。因此咱們須要把這些方法拆分紅獨立的職責,可讓一個類只負責一個方法,每一個類只專心處理本身的方法便可。
單一職責原則的優勢:
類的複雜性下降,實現什麼職責都有明確的定義;
邏輯變得簡單,類的可讀性提升了,並且,由於邏輯簡單,代碼的可維護性也提升了;
變動的風險下降,由於只會在單一的類中的修改。
開閉原則
開閉原則,Open Closed Principle,是Java世界裏最基礎的設計原則,其定義是:
一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉
也就是說,一個軟件實體應該經過擴展來實現變化,而不是經過修改已有的代碼實現變化。這是爲軟件實體的將來事件而制定的對現行開發設計進行約束的一個原則。
在咱們編碼的過程當中,需求變化是不斷的發生的,當咱們須要對代碼進行修改時,咱們應該儘可能作到能不動原來的代碼就不動,經過擴展的方式來知足需求。
遵循開閉原則的最好手段就是抽象,例如前面單一職責原則舉的工程師類,咱們說的是把方法抽離成單獨的類,每一個類負責單一的職責,但其實從開閉原則的角度說,更好的方式是把職責設計成接口,例如把寫代碼的職責方法抽離成接口的形式,同時,咱們在設計之初須要考慮到將來全部可能發生變化的因素,好比將來有可能由於業務須要分紅後臺和前端的功能,這時設計之初就能夠設計成兩個接口,
public interface BackCode{
void writeCode();
}
public interface FrontCode{
void writeCode();
}
若是未來前端代碼的業務發生變化,咱們只需擴展前端接口的功能,或者修改前端接口的實現類便可,後臺接口以及實現類就不會受到影響,這就是抽象的好處。
里氏替換原則
里氏替換原則,英文名Liskov Substitution Principle,它的定義是
若是對每個類型爲T1的對象o1,都有類型爲T2的對象o2,使得以T1定義的全部程序P在全部對象o1都替換成o2的時候,程序P的行爲都沒有發生變化,那麼類型T2是類型T1的子類型。
看起來有點繞口,它還有一個簡單的定義:
全部引用基類的地方必須可以透明地使用其子類的對象。
通俗點說,只要父類能出現的地方子類就能夠出現,並且替換爲子類也不會產生任何異常。 可是反過來就不行了,由於子類能夠擴展父類沒有的功能,同時子類還不能改變父類原有的功能。
咱們都知道,面向對象的三大特徵是封裝、繼承和多態,這三者缺一不可,但三者之間卻並不 「和諧「。由於繼承有不少缺點,當子類繼承父類時,雖然能夠複用父類的代碼,可是父類的屬性和方法對子類都是透明的,子類能夠隨意修改父類的成員。若是需求變動,子類對父類的方法進行了一些複寫的時候,其餘的子類可能就須要隨之改變,這在必定程度上就違反了封裝的原則,解決的方案就是引入里氏替換原則。
里氏替換原則爲良好的繼承定義了一個規範,它包含了4層含義:
一、子類能夠實現父類的抽象方法,可是不能覆蓋父類的非抽象方法。
二、子類能夠有本身的個性,能夠有本身的屬性和方法。
三、子類覆蓋或重載父類的方法時輸入參數能夠被放大。
好比父類有一個方法,參數是HashMap
public class Father {
public void test(HashMap map){
System.out.println("父類被執行。。。。。");
}
}
那麼子類的同名方法輸入參數的類型能夠擴大,例如咱們輸入參數爲Map,
public class Son extends Father{
public void test(Map map){
System.out.println("子類被執行。。。。");
}
}
咱們寫一個場景類測試一下父類的方法執行效果,
public class Client {
public static void main(String[] args) {
Father father = new Father();
HashMap map = new HashMap();
father.test(map);
}
}
結果輸出:父類被執行。。。。。
由於里氏替換原則,只要父類能出現的地方子類就能夠出現,並且替換爲子類也不會產生任何異常。咱們改下代碼,調用子類的方法,
public class Client {
public static void main(String[] args) {
Son son = new Son();
HashMap map = new HashMap();
father.test(map);
}
}
運行結果是同樣的,由於子類方法的輸入參數類型範圍擴大了,子類代替父類傳遞到調用者中,子類的方法永遠不會被執行,這樣的結果實際上是正確的,若是想讓子類方法執行,能夠重寫方法體。
反之,若是子類的輸入參數類型範圍比父類還小,好比父類中的參數是Map,而子類是HashMap,那麼執行上述代碼的結果就會是子類的方法體,有人說,這難道不對嗎?子類顯示本身的內容啊。其實這是不對的,由於子類沒有複寫父類的同名方法,方法就被執行了,這會引發邏輯的混亂,若是父類是抽象類,子類是實現類,你傳遞一個這樣的實現類就違背了父類的意圖了,容易引發邏輯混亂,因此子類覆蓋或重載父類的方法時輸入參數一定是相同或者放大的。
四、子類覆蓋或重載父類的方法時輸出結果能夠被縮小,也就是說返回值要小於或等於父類的方法返回值。
確保程序遵循里氏替換原則能夠要求咱們的程序創建抽象,經過抽象去創建規範,而後用實現去擴展細節,因此,它跟開閉原則每每是相互依存的。
依賴倒置原則
依賴倒置原則,Dependence Inversion Principle,簡稱DIP,它的定義是:
高層模塊不該該依賴底層模塊,二者都應該依賴其抽象;
抽象不該該依賴細節;
細節應該依賴抽象;
什麼是高層模塊和底層模塊呢?不可分割的原子邏輯就是底層模塊,原子邏輯的再組裝就是高層模塊。
在Java語言中,抽象就是指接口或抽象類,二者都不能被實例化;而細節就是實現接口或繼承抽象類產生的類,也就是能夠被實例化的實現類。依賴倒置原則是指模塊間的依賴是經過抽象來發生的,實現類之間不發生直接的依賴關係,其依賴關係是經過接口是來實現的,這就是俗稱的面向接口編程。
咱們用歌手唱歌來舉例,好比一個歌手唱國語歌,用代碼表示就是:
public class ChineseSong {
public String language() {
return "國語歌";
}
}
public class Singer {
//唱歌的方法
public void sing(ChineseSong song) {
System.out.println("歌手" + song.language());
}
}
public class Client {
public static void main(String[] args) {
Singer singer = new Singer();
ChineseSong song = new ChineseSong();
singer.sing(song);
}
}
運行main方法,結果就會輸出:歌手唱國語歌
如今,咱們須要給歌手加一點難度,好比說唱英文歌,在這個類中,咱們發現是很難作的。由於咱們Singer類依賴於一個具體的實現類ChineseSong,也許有人會說能夠在加一個方法啊,但這樣一來咱們就修改了Singer類了,若是之後須要增長更多的歌種,那歌手類不是一直要被修改?也就是說,依賴類已經不穩定了,這顯然不是咱們想看到的。
因此咱們須要用面向接口編程的思想來優化咱們的方案,改爲以下的代碼:
public interface Song {
public String language();
}
public class ChineseSong implements Song{
public String language() {
return "唱國語歌";
}
}
public class EnglishSong implements Song {
public String language() {
return "唱英語歌";
}
}
public class Singer {
//唱歌的方法
public void sing(Song song) {
System.out.println("歌手" + song.language());
}
}
public class Client {
public static void main(String[] args) {
Singer singer = new Singer();
EnglishSong englishSong = new EnglishSong();
// 唱英文歌
singer.sing(englishSong);
}
}
咱們把歌單獨抽成一個接口Song,每一個歌種都實現該接口並重寫方法,這樣一來,歌手的代碼沒必要改動,若是須要添加歌的種類,只需寫多一個實現類繼承Song便可。
經過這樣的面向接口編程,咱們的代碼就有了更好的擴展性,同時也下降了耦合,提升了系統的穩定性。
接口隔離原則
接口隔離原則,Interface Segregation Principle,簡稱ISP,其定義是:
客戶端不該該依賴它不須要的接口
意思就是客戶端須要什麼接口就提供什麼接口,把不須要的接口剔除掉,這就須要對接口進行細化,保證接口的純潔性。換成另外一種說法就是,類間的依賴關係應該創建在最小的接口上,也就是創建單一的接口。
你可能會疑惑,創建單一接口,這不是單一職責原則嗎?其實不是,單一職責原則要求的是類和接口職責單一,注重的是職責,一個職責的接口是能夠有多個方法的,而接口隔離原則要求的是接口的方法儘可能少,模塊儘可能單一,若是須要提供給客戶端不少的模塊,那麼就要相應的定義多個接口,不要把全部的模塊功能都定義在一個接口中,那樣會顯得很臃腫。
舉個例子,如今的智能手機很是的發達,幾乎是人手一部的社會狀態,在咱們年輕人的觀念裏,好的智能手機應該是價格便宜,外觀好看,功能豐富的,由此咱們能夠定義一個智能手機的抽象接口 ISmartPhone,代碼以下所示:
public interface ISmartPhone {
public void cheapPrice();
public void goodLooking();
public void richFunction();
}
接着,咱們定義一個手機接口的實現類,實現這三個抽象方法,
public class SmartPhone implements ISmartPhone{
public void cheapPrice() {
System.out.println("這手機便宜~~~~~");
}
public void goodLooking() {
System.out.println("這手機外觀好看~~~~~");
}
public void richFunction() {
System.out.println("這手機功能真多~~~~~");
}
}
而後,定義一個用戶的實體類 User,並定義一個構造方法,以ISmartPhone 做爲參數傳入,同時,咱們也定義一個使用的方法usePhone 來調用接口的方法,
public class User {
private ISmartPhone phone;
public User(ISmartPhone phone){
this.phone = phone;
}
public void usePhone(){
phone.cheapPrice();
phone.goodLooking();
phone.richFunction();
}
}
能夠看出,當咱們實例化User類並調用其方法usePhone後,控制檯上就會顯示手機接口三個方法的方法體信息,這種設計看上去沒什麼大毛病,可是咱們能夠仔細想下,ISmartPhone這個接口的設計是否已經達到最優了呢?很遺憾,答案是沒有,接口其實還能夠再優化。
由於除了年輕人以外,中年商務人士也在用智能手機,在他們的觀念裏,智能手機並不須要豐富的功能,甚至不用考慮是否便宜 (有錢就是任性~~~~),由於成功人士都比較忙,對智能手機的要求大可能是外觀大氣,功能簡單便可,這纔是他們心中好的智能手機的特徵,這樣一來,咱們定義的 ISmartPhone 接口就沒法適用了,由於咱們的接口定義了智能手機必須知足三個特性,若是實現該接口就必須三個方法都實現,而對商務人員的標準來講,咱們定義的方法只有外觀符合且能夠重用而已。你可能會說,我能夠重寫一個實現類啊,只實現外觀的方法,另外兩個方法置空,什麼都不寫,這不就好了嗎?可是這也不行,由於 User 引用的是ISmartPhone 接口,它調用三個方法,你只實現了兩個,那麼打印信息就少了兩條了,只靠外觀的特性,使用者怎麼知道智能手機是否符合本身的預期?
分析到這裏,咱們大概就明白了,其實ISmartPhone的設計是有缺陷的,過於臃腫了,按照接口隔離原則,咱們能夠根據不一樣的特性把智能手機的接口進行拆分,這樣一來,每一個接口的功能就會變得單一,保證了接口的純潔性,也進一步提升了代碼的靈活性和穩定性。
迪米特原則
迪米特原則,Law of Demeter,簡稱LoD,也被稱爲最少知識原則,它描述的規則是:
一個對象應該對其餘對象有最少的瞭解
也就是說,一個類應該對本身須要耦合或調用的類知道的最少,類與類之間的關係越密切,耦合度越大,那麼類的變化對其耦合的類的影響也會越大,這也是咱們面向設計的核心原則:低耦合,高內聚。
迪米特法則還有一個解釋:只與直接的朋友通訊。
什麼是直接的朋友呢?每一個對象都必然與其餘對象有耦合關係,兩個對象的耦合就成爲朋友關係,這種關係的類型不少,例如組合、聚合、依賴等。其中,咱們稱出現成員變量、方法參數、方法返回值中的類爲直接的朋友,而出如今局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要做爲局部變量的形式出如今類的內部。
舉個例子,上體育課以前,老師讓班長先去體務室拿20個籃球,等下上課的時候要用。根據這一場景,咱們能夠設計出三個類 Teacher(老師),Monitor (班長) 和 BasketBall (籃球),以及發佈命令的方法command 和 拿籃球的方法takeBall,
public class Teacher {
// 命令班長去拿球
public void command(Monitor monitor) {
List<BasketBall> ballList = new ArrayList<BasketBall>();
// 初始化籃球數目
for (int i = 0;i<20;i++){
ballList.add(new BasketBall());
}
// 通知班長開始去拿球
monitor.takeBall(ballList);
}
}
public class BasketBall {
}
public class Monitor {
// 拿球
public void takeBall(List<BasketBall> balls) {
System.out.println("籃球數目:" + balls.size());
}
}
而後,咱們寫一個情景類進行測試:
public class Client {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.command(new Monitor());
}
}
結果顯示以下:
籃球數目:20
雖然結果是正確的,但咱們的程序其實仍是存在問題,由於從場景來講,老師只需命令班長拿籃球便可,Teacher只須要一個朋友----Monitor,但在程序裏,Teacher的方法體中卻依賴了BasketBall類,也就是說,Teacher類與一個陌生的類有了交流,這樣Teacher的健壯性就被破壞了,由於一旦BasketBall類作了修改,那麼Teacher也須要作修改,這很明顯違背了迪米特法則。
所以,咱們須要對程序作些修改,在Teacher的方法中去掉對BasketBall類的依賴,只讓Teacher類與朋友類Monitor產生依賴,修改後的代碼以下:
public class Teacher {
// 命令班長去拿球
public void command(Monitor monitor) {
// 通知班長開始去拿球
monitor.takeBall();
}
}
public class Monitor {
// 拿球
public void takeBall() {
List<BasketBall> ballList = new ArrayList<BasketBall>();
// 初始化籃球數目
for (int i = 0;i<20;i++){
ballList.add(new BasketBall());
}
System.out.println("籃球數目:" + ballList.size());
}
}
這樣一來,Teacher類就不會與BasketBall類產生依賴了,即時往後由於業務須要修改BasketBall也不會影響Teacher類。
總結
好了,面向對象的六大原則就介紹到這裏了。其實,咱們不難發現,六大原則雖然說是原則,但它們並非強制性的,更多的是建議。遵守這些原則當然能幫助咱們更好的規範咱們的系統設計和代碼習慣,但並非全部的場景都適用,就例如接口隔離原則,在現實系統開發中,咱們很難徹底遵照一個模塊一個接口的設計,不然業務多了就會出現代碼設計過分的狀況,讓整個系統變得過於龐大,增長了系統的複雜度,甚至影響本身的項目進度,得不償失啊。
因此,仍是那句話,在合適的場景選擇合適的技術!
參考:《設計模式之禪》