在軟件設計和實際的開發過程中,軟件設計應該秉承一些基本的原則。除了須要考慮軟件開發所需的時間、成本、質量以外,還須要考慮到軟件系統的穩定性、可維護性、可擴展性等。本文將對軟件的7大設計原則進行一個講解和配套以實際業務場景,便於讀者更好的理解軟件設計的基本原則。
另外,在軟件設計中,軟件設計原則不是必定要所有遵照,而是須要根據實際的業務場景須要來作一個取捨,達到一個平衡。也許這就是軟件設計的藝術吧。php
定義:一個軟件實體如類、模塊、函數應該對擴展開放,對修改關閉
特色:用抽象構建框架,用實現擴展細節
優勢:提升軟件系統的可複用性和可維護性java
開閉原則是軟件系統設計最基本的原則。實現開閉原則的思想就是面向抽象編程。由於通常來講,抽象是穩定的。實現的細節是不可知的。python
業務場景
某培訓機構分別開設java、python、php課程,每一個課程都包含課程id、課程名、課程價格信息。在使用uml建模的時候,他們的類結構關係以下:
從模型中能夠看出,ICourse爲頂層父類,JavaCourse,PythonCourse,PhpCourse三個類爲實現類。他們分別實現了父類的獲取課程id、課程信息、課程價格的方法。這是最初的軟件版本。隨着時間的推移,培訓機構爲了擴展業務和吸引更多的學生來辦理培訓課程,須要實現可以對已有的課程進行打折活動,這個時候咱們就須要對軟件進行一個版本的升級。
此時咱們有兩種方法來實現:
第一種方法:好比是對Java課程打折。直接修改JavaCourse的源代碼,在裏面新增一個方法getDiscountPrice()或者使用已有的getPrice()方法進行修改獲取折扣價。phthon和php課程同理。
第二種方法:採用面向抽象的方法來編程,不直接修改類代碼,而是經過類繼承來實現。
使用第一種方法的好處是簡單、快速。可是當一個系統很是複雜和龐大的時候,越是對底層的代碼進行修改就越有可能帶來潛在的危機。你不能肯定對類中方法的修改會不會影響到其餘的方法調用,是否會帶來不可思議的災難。
使用第二種方法的壞處是可能須要多花費一點時間和精力。可是優勢也是顯而易見的。即不會影響原有的邏輯。在新的軟件開發版本中,即擴展了功能,又不影響以前的業務邏輯。提升了系統的可維護性和可獲展性。編程
開閉原則,就是在開發過程中,儘可能抽象出框架去進行擴展,杜絕對已有模塊的修改。框架
定義:高層調用不該該依賴底層模塊,兩者都應該依賴其抽象
特色:高層不依賴細節,細節依賴抽象
優勢:模塊之間彼此獨立,互不影響,實現模塊之間的鬆耦合編程語言
業務場景
仍是上面的培訓機構,如今小明報了三門課程,實現的類圖以下
函數
實現僞代碼以下:學習
XiaoMing{ public void studyJavaCourse(JavaCourse java); public void pythonJavaCourse(pythonCourse java); public void phpJavaCourse(phpCourse java); } // 假設提供了帶參數構造函數,參數分別爲課程id,課程名稱,課程單價 main{ XiaoMing xiaoMing = new XiaoMing(); xiaoMing.studyJavaCourse(new JavaCourse(1,"java",100.0)); xiaoMing.studyPhpCourse(new PhpCourse(2,"php",200.0)); xiaoMing.studyPythonCourse(new PythonCourse(3,"python",300.0)); }
這是沒有用到依賴倒置原則時候的寫法。在實際開發中這種寫法極大的下降了代碼之間的耦合度和靈活性。下面看看使用到了依賴倒置原則的寫法。
spa
這個時候,當咱們須要取得學習課程信息的僞代碼以下:設計
XiaoMing{ public void studyCouser(ICouser couser); } // 假設提供了帶參數構造函數,參數分別爲課程id,課程名稱,課程單價 main{ XiaoMing xiaoMing = new XiaoMing(); xiaoMing.study(new JavaCourse(1,"java",100.0)); xiaoMing.study(new PhpCourse(2,"php",200.0)); xiaoMing.study(new PythonCourse(3,"python",300.0)); }
不知道你們看明白了沒有(注意看XiaoMing對象),一樣的實現功能,高層直接調用底層代碼,增長了代碼的複雜度。而調用底層的抽象,只提供一個對外接口就能實現功能。
定義:不要存在致使類變動的多個緣由。
特色:接口、類、方法負責的職責是單一的
優勢:提升代碼的可讀性、可維護性、下降更改代碼的風險
業務場景
依舊是培訓機構的課程,如今要在課程的基礎上新增購買課程和課程退款。接口的非單一職責僞代碼以下:
public interface ICourse{ getPrice(); getCourseName(); buyCouse(); refundCourse(); }
該接口抽象出了「獲取課程信息」,「課程慣例」兩個職責,那麼獲取課程信息相關子類在實現父類的時候,也須要實現課程管理的職責相關方法,即便這徹底沒有必要。
咱們再來看下方法的非單一職責例子,僞代碼以下:
public void getIdOrName(String properties){ if(properties.equals("id")){ System.out.println("id"); }else{ System.out.println("name"); } }
在上面的代碼中,getIdOrName即能輸出課程id,也能輸出課程名稱。固然這裏代碼足夠簡單,看不出什麼。可是若是在複雜點,因爲調用邏輯的不清晰,可能會致使後續開發人員的困惑。影響代碼的可讀性。同時,在對這一塊代碼進行修改的時候,出現錯誤的狀況下可能會致使另外一職能的錯誤。爲了寫出更加清晰的代碼,應該寫成以下方式:
public void getId(){……}; public void getName(){……};
這樣,即便獲取課程id的方法出錯了,也不會影響到獲取課程名稱的相關功能。
定義:用單個專門的接口,而不是一個總的接口
特色:
1)一個類對另外一個類依賴的接口儘量簡單 2)不斷細化接口,但也要適度
優勢:提升代碼的靈活性和可讀性,符合高內聚低耦合思想。
業務場景
在動物身上的行爲有走、飛、游泳。設計一個頂層父類的接口,僞代碼以下:
public interface Animals{ walk(); fly(); swimming(); }
如今有一隻鳥和一隻狗,實現了動物類
// 狗 Dog implments Animals{ walk(){ } fly(){ } swimming(){ } } // 鳥 Bird implments Animals{ walk(){ } fly(){ } swimming(){ } }
能夠看出,不論是狗仍是鳥,都實現了動物走、飛、游泳的行爲。但是狗不會飛,鳥類也不必定會游泳。有些鳥也不必定會飛。那麼針對更加細化的對象來講,徹底沒有實現父類全部接口的必要。這個時候,咱們就須要對接口進行一個細化,也就是接口隔離,增長接口的細粒度。僞代碼以下:
interface Animals{ } interface Walk implments Animals{ walk(); } interface Fly implments Animals{ fly(); } interface Swimging implments Animals{ swimging(); } // 狗 Dog implments Walk,Swimging{ walk(); swimging(); } // 鳥 Bird implments Fly,Walk{ fly(); walk(); }
這樣,經過上面細化的接口,就能根據實際的須要去實現專門的接口。須要注意的是,適當的接口細化能提升代碼的靈活性,但也不是越多越好,細化得越多,相應的系統複雜度也會增長。
引伸:單一職責和接口隔離職責差別
單一職責強調得是類、接口、方法實現細節的功能惟一性,接口隔離是對接口的限制,是在抽象層面對接口進行的高內聚低耦合實現。
定義:對一個類的瞭解的越少越好
特色:只對朋友交流,不和陌生人打交道
優勢:下降類之間的耦合度。
這裏的朋友是指具備組合、依賴關係和類。對於其餘沒有關係的類不要引入類中。具體詳細瞭解請看下面的業務場景。
業務場景
老闆去查看團隊的員工數量。僞代碼以下:
Boss{ checkTeam(Team t){ List<People> p = new ArrayList<People>(); for(int i=0;i<10;i++){ p.add(new People()); } t.countPeople(List<People> p); // 輸出團隊成員人數 } } Team{ countPeople(List<People> p){ System.out.println(p.length()); } }
在本例中的類關係如圖:
老闆和員工是依賴關係(員工類是方法的參數),可是老闆的檢查員工方法中卻出現和老闆無關的People類,這裏陌生人依賴(People類)也依賴咱們的老闆類。這就違背了咱們的最少知道原則。
按照最少知道原則,他們之間的關係應該是People類是Team類的依賴,Boss類是Team類的依賴。
僞代碼以下:
Boss{ checkTeam(Team t){ t.countPeople(); // 輸出團隊成員人數 } } Team{ countPeople(List<People> p){ List<People> p = new ArrayList<People>(); for(int i=0;i<10;i++){ p.add(new People()); } System.out.println(p.length()); } }
最少知道原則下降了類之間的耦合度。在實際開發中,應該遵循使用。
定義:父類出現的地方,子類也能夠出現,而且子類替換父類後,不會產生任何問題。
特色:里氏替換原則包含了四層含義,以下
1) 子類必須徹底實現父類的方法 2)子類能夠擁有本身的個性 3)覆蓋或實現父類方法時,輸入參數能夠被放大 4)覆蓋或實現父類方法時,返回結果能夠被縮小
定義:在一個新的對象裏使用已有對象,經過委派該對象來實現對已有功能的服用。
特色:符合 Has—A 關係
優勢:提升了代碼的封裝性,相比較於繼承,隱蔽了代碼的細節。
爲何須要使用組合/複合原則?在什麼狀況下使用?
實現代碼功能的複用,有兩種方法,一種是繼承,一種是組合。使用繼承的時候,須要遵循里氏替換原則,咱們在修改父類基類的時候,相關的子類也須要作一個修改。這無疑提升的代碼的維護性。使用組合的方法,經過委派成員變量複用對象功能,該對象功能的調整,並不會影響到委託類的代碼。代碼可維護性更高。咱們經過下面人的屬性來比較如下繼承和組合的差別。
做爲一個社會人,有多重屬性,繼承僞代碼以下
Person{ public Person(){} } Teacher extends Person{ System.out.println("teacher"); } Student extends People{ System.out.println("student"); }
經過以上方式,能夠看到,咱們新建立一個Person其子類對象的時候,這個子類只能是老師或者只能是學生。可是實際生活當中,一我的的屬性是多樣的,因此在建立對象的時候,要可以實現他多是老師,也有多是學生的要求。這個時候就得用組合方式了。僞代碼以下:
Person{ private Role role; setRole(){}; getRole(){}; } interface Role{} Teacher implements Role{ } Student implements Role{ }
在上面方式中,動態應用的時候,經過Person的getPerson方法調用對象身份屬性。在setPerson時用的是那個對象就是哪一個身份屬性。那麼咱們如何判斷在什麼狀況下合理使用繼承或者組合呢,其實很簡單,咱們若是不能斷定一個子類在將來是否會變成別的子類的時候,就是用組合的方式。若是一個子類的對象信息是固定不變的,就是用繼承。還有一種方式,就是使用抽象的思惟,看是is-a關係,仍是has-a關係。is-a使用繼承方式。has-a使用組合方式
縱觀這些原則,針對的是面向對象編程的思想,關注點是編程語言的封裝性、繼承性、多態性。經過這三大特性,是軟件框架變得更加清晰,可維護性更好,可擴展性更強。但願自身在實際應用中,可以合理使用這些原則。