您好,登錄后才能下訂單哦!
這篇文章主要講解了“面向對象設計的六大原則是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“面向對象設計的六大原則是什么”吧!
言歸正傳,這是我學習設計模式系列的第一篇文章,本文主要講的是面向對象設計應該遵循的六大原則,掌握這些原則能幫助我們更好的理解面向對象的概念,也能更好的理解設計模式。這六大原則分別是:
單一職責原則——SRP
開閉原則——OCP
里式替換原則——LSP
依賴倒置原則——DIP
接口隔離原則——ISP
迪米特原則——LOD
單一職責原則,Single Responsibility Principle,簡稱SRP。其定義是應該有且僅有一個類引起類的變更,這話的意思就是一個類只擔負一個職責。
舉個例子,在創業公司里,由于人力成本控制和流程不夠規范的原因,往往一個人需要擔任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層含義:
1、子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
2、子類可以有自己的個性,可以有自己的屬性和方法。
3、子類覆蓋或重載父類的方法時輸入參數可以被放大。
比如父類有一個方法,參數是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,那么執行上述代碼的結果就會是子類的方法體,有人說,這難道不對嗎?子類顯示自己的內容啊。其實這是不對的,因為子類沒有復寫父類的同名方法,方法就被執行了,這會引起邏輯的混亂,如果父類是抽象類,子類是實現類,你傳遞一個這樣的實現類就違背了父類的意圖了,容易引起邏輯混亂,所以子類覆蓋或重載父類的方法時輸入參數必定是相同或者放大的。
4、子類覆蓋或重載父類的方法時輸出結果可以被縮小,也就是說返回值要小于或等于父類的方法返回值。
確保程序遵循里氏替換原則可以要求我們的程序建立抽象,通過抽象去建立規范,然后用實現去擴展細節,所以,它跟開閉原則往往是相互依存的。
依賴倒置原則,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類。
感謝各位的閱讀,以上就是“面向對象設計的六大原則是什么”的內容了,經過本文的學習后,相信大家對面向對象設計的六大原則是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。