設計模式(Design Pattern)是前輩們在代碼實踐中所總結的經驗,是解決某些特定問題的套路。在使用一些優秀的框架時,可能會接觸到它裏面所運用到的一些設計模式,又或許你在編碼去設計一些模塊時,爲了提升代碼可複用性、擴展性、可讀性等,運用到的一些設計理念也會與某些設計模式思想相吻合。java
系統的瞭解和學習設計模式是頗有必要的,能幫助提高面對對象設計的能力,瞭解各類設計模式的特色和運用場景數據庫
在學習設計模式前,先了解下面對對象的設計原則編程
對於一個好的面對對象軟件系統的設計來講,可維護性和可複用性是很重要的,如何同時提升一個系統的可維護性和可複用性是面對對象設計須要解決的核心問題之一。設計模式
在面對對象設計中,面對對象設計原則是爲了去支持可維護性和可複用性的,這些原則會體如今不少的設計模式中,也就是說這些設計原則實際上就是從這些設計方案中總結提取出來的指導性原則。框架
最多見的7種面向對象設計原則編程語言
設計原則名稱 | 定義 |
---|---|
開閉原則(Open-Closed Principle, OCP) | 軟件實體應對擴展開放,而對修改關閉 |
單一職責原則(Single Responsibility Principle, SRP) | 一個類只負責一個功能領域中的相應職責 |
里氏代換原則(Liskov Substitution Principle, LSP) | 全部引用基類對象的地方可以透明地使用其子類的對象 |
依賴倒轉原則(Dependence Inversion Principle, DIP) | 抽象不該該依賴於細節,細節應該依賴於抽象 |
接口隔離原則(Interface Segregation Principle, ISP) | 使用多個專門的接口,而不使用單一的總接口 |
合成複用原則(Composite Reuse Principle,CRP) | 儘可能使用對象組合,而不是繼承來達到複用的目的 |
迪米特法則(Law of Demeter, LoD) | 一個軟件實體應當儘量少地與其餘實體發生相互做用 |
開閉原則(開放-封閉原則)有兩個特徵,對擴展是開放的(Open for extension),對修改是封閉的(Open for modification)。也就是說一個軟件實體(模塊、類、函數等等)要實現變化,應該是經過擴展而不是修改已有的代碼函數
任何的軟件在其生命週期內需求均可能會發生變化,既然變化是必然的,咱們就應該在設計時儘可能適應這些變化,以提升項目的穩定性和靈活性。若是一個軟件設計符合開閉原則,那麼能夠很是方便地對系統進行擴展,並且在擴展時無須修改現有代碼,使得軟件系統在擁有適應性和靈活性的同時具有較好的穩定性和延續性。隨着軟件規模愈來愈大,軟件壽命愈來愈長,軟件維護成本愈來愈高,設計知足開閉原則的軟件系統也變得愈來愈重要學習
爲了知足開閉原則,須要對系統進行抽象化設計,抽象化是開閉原則的關鍵。設計模塊時,對最可能發生變化的地方,經過構造抽象來隔離這些變化。在Java、C#等編程語言中,能夠爲系統定義一個相對穩定的抽象層,而將不一樣的實現行爲移至具體的實現層中完成。在不少面向對象編程語言中都提供了接口、抽象類等機制,能夠經過它們定義系統的抽象層,再經過具體類來進行擴展。若是須要修改系統的行爲,無須對抽象層進行任何改動,只須要增長新的具體類來實現新的業務功能便可,實如今不修改已有代碼的基礎上擴展系統的功能,達到開閉原則的要求this
這裏舉一個簡單的例子,某個系統中某個功能能夠來顯示各類類型的圖表,好比餅圖和柱狀圖。開始的設計方案以下:編碼
ChartDisplay中的display方法以下
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
複製代碼
在這個例子中,假如我須要添加新的圖表對象(折線圖LineChart),那麼我須要在ChartDisplay中的display方法中去添加新的判斷邏輯,這是不符合開閉原則。ChartDisplay類是用來作圖表的顯示工做,但具體的圖表是變化的,須要將這些變化隔離出來
抽象化的方法:
重構後的結構以下
如上,ChartDisplay只針對抽象類AbstractChart編程,經過setChart來得到具體的圖表對象,dispalay方法中直接執行 chart.display(),當咱們要新增新的圖表,那麼直接建立圖表子類繼承AbstractChart,並實現本身的display方法就好,並不須要修改已有的代碼。
單一職責原則(Single Responsibility Principle, SRP):一個類應該只有一個職責,對外只提供一種功能,應該有且僅有一個緣由引發類的變化
能力越大,責任越大?咱們不能建立一個「超級類」,能解決全部的事情,相反,一個類(大到模塊,小到方法)所承擔的責任越多,那麼他被複用的可能性就越小。並且一個類承擔的職責過多,這些職責耦合度會很高,當其中一個職責變化時,可能會影響其餘職責的運做,所以要將這些職責進行分離,將不一樣的職責封裝在不一樣的類中,將不一樣的變化緣由封裝在不一樣的類中,若是多個職責老是同時發生改變則可將它們封裝在同一類中
單一職責原則,用於控制類的粒度大小,實現高內聚、低耦合,它是最簡單但又最難運用的原則,如何發現類的不一樣職責並將其分離,須要具備較強的分析設計能力和相關實踐經驗。若是你可以想到多於一個動機去改變一個類,那麼這個類就有多於一個的職責,就要考慮類的職責分離
記得在剛入門Java接觸到 JDBC的時候,爲了實現查詢學生列表,一口氣從數據庫的鏈接到數據查詢再到數據展現,簡直「一鼓作氣」,但這種面向過程式的編程卻沒有很好的擴展性,當我想要再實現其餘功能時,將會有大量重複的代碼,而重複的地方須要修改,那就更麻煩了。後來稍微改進了,創建了只負責數據庫鏈接資源的類DBUtil,再到後來使用持久層的框架。職責劃分後,開發時便只需關注業務的處理
單一職責適用於接口、類,同時也適用於方法,一個方法儘量作一件事情,好比一個方法修改用戶密碼,不要把這個方法放到「修改用戶信息」方法中,這個方法的顆粒度很粗
上面的方法就任責不清晰,不單一,下面替換成具體的修改動做,經過命名咱們就能知曉方法的大概處理邏輯
里氏代換原則(Liskov Substitution Principle, LSP):全部引用基類(父類)的地方必須能透明地使用其子類的對象
里氏代換原則告訴咱們,在軟件中,只要父類能出現的地方子類就能夠出現,並且替換爲子類也不會產生任何錯誤或異常,程序將不會產生任何錯誤和異常,反過來則不成立,若是一個軟件實體使用的是一個子類對象的話,那麼它不必定可以使用基類對象
裏式替換才使得開發-封閉成爲可能,子類的可替代性才使得使用父類類型的地方能夠在無需修改的狀況下就能夠擴展。里氏代換原則是實現開閉原則的重要方式之一,因爲使用基類對象的地方均可以使用子類對象,所以在程序中儘可能使用基類類型來對對象進行定義,而在運行時再肯定其子類類型,用子類對象來替換父類對象
若是說開閉原則是面向對象設計的目標的話,那麼依賴倒轉原則就是面向對象設計的主要實現機制之一,它是系統抽象化的具體實現
依賴倒轉原則(Dependency Inversion Principle, DIP):高層模塊不該該依賴低層模塊,二者都應該依賴其抽象;抽象不該該依賴細節,細節應該依賴抽象
上面的定義有些彆扭,引入《設計模式之禪》的話來講明依賴倒轉
高層模塊和低層模塊容易理解,每個邏輯的實現都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。那什麼是抽象?什麼又是細節呢?在Java語言中,抽象就是指接口或抽象類,二者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特色就是能夠直接被實例化,也就是能夠加上一個關鍵字new產生一個對象。
依賴倒置原則在Java語言中的表現就是:
更精簡的定義就是要面向接口編程(Object-Oriented Design),而不是針對實現編程
看到依賴倒轉和它的定義,是否會想起Spring的依賴注入(Dependency Injection, DI)和控制反轉(Inversion of Control,IOC),一般咱們使用Spring的IoC容器時,會聲明依賴的接口,在程序運行時肯定具體的實現類並注入。這樣便下降了類間的耦合性、提升了系統的穩定性
接口隔離原則(Interface Segregation Principle, ISP):使用多個專門的接口,而不使用單一的總接口, 即客戶端不該該依賴那些它不須要的接口
根據接口隔離原則,當一個接口太大時,咱們須要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法便可。每個接口應該承擔一種相對獨立的角色,不幹不應乾的事,該乾的事都要幹。這裏的「接 口」每每有兩種不一樣的含義:一種是指一個類型所具備的方法特徵的集合,僅僅是一種邏輯上的抽象;另一種是指某種語言具體的「接口」定義,有嚴格的定義和結構,好比Java語言中的interface。對於這兩種不一樣的含義,ISP的表達方式以及含義都有所不一樣:
(1) 當把「接口」理解成一個類型所提供的全部方法特徵的集合的時候,這就是一種邏輯上的概念,接口的劃分將直接帶來類型的劃分。能夠把接口理解成角色,一個接口只能表明一個角色,每一個角色都有它特定的一個接口,此時,這個原則能夠叫作「角色隔離原則」。
(2) 若是把「接口」理解成狹義的特定語言的接口,那麼ISP表達的意思是指接口僅僅提供客戶端須要的行爲,客戶端不須要的行爲則隱藏起來,應當爲客戶端提供儘量小的單獨的接口,而不要提供大的總接口。在面向對象編程語言中,實現一個接口就須要實現該接口中定義的全部方法,所以大的總接口使用起來不必定很方便,爲了使接口的職責單一,須要將大接口中的方法根據其職責不一樣分別放在不一樣的小接口中,以確保每一個接口使用起來都較爲方便,並都承擔某一單一角色。接口應該儘可能細化,同時接口中的方法應該儘可能少,每一個接口中只包含一個客戶(如子模塊或業務邏輯類)所需的方法便可,這種機制也稱爲「定製服務」,即爲不一樣的客戶端提供寬窄不一樣的接口。
接口隔離原則和單一職責都是爲了提升類的內聚性、下降它們之間的耦合性,體現了封裝的思想,但二者是不一樣的:
合成複用原則又稱爲組合/聚合複用原則(Composition/Aggregate Reuse Principle, CARP)
合成複用原則(Composite Reuse Principle, CRP):儘可能使用對象組合,而不是繼承來達到複用的目的
合成複用原則就是在一個新的對象裏經過關聯關係(包括組合關係和聚合關係)來使用一些已有的對象,使之成爲新對象的一部分;新對象經過委派調用已有對象的方法達到複用功能的目的。簡言之:複用時要儘可能使用組合/聚合關係(關聯關係),少用繼承
在面向對象設計中,能夠經過兩種方法在不一樣的環境中複用已有的設計和實現,即經過組合/聚合關係或經過繼承,但首先應該考慮使用組合/聚合,組合/聚合可使系統更加靈活,下降類與類之間的耦合度,一個類的變化對其餘類形成的影響相對較少;其次才考慮繼承,在使用繼承時,須要嚴格遵循里氏代換原則,有效使用繼承會有助於對問題的理解,下降複雜度,而濫用繼承反而會增長系統構建和維護的難度以及系統的複雜度,所以須要慎重使用繼承複用
繼承複用的主要問題在於繼承複用會破壞系統的封裝性:
組合或聚合關係能夠將已有的對象到新對象中,使之成爲新對象的一部分
通常而言,若是兩個類之間是「Has-A」的關係應使用組合或聚合,若是是「Is-A」關係可以使用繼承。"Is-A"是嚴格的分類學意義上的定義,意思是一個類是另外一個類的"一種";而"Has-A"則不一樣,它表示某一個角色具備某一項責任。
迪米特法則(Law of Demeter,LoD)也稱爲最少知識原則(Least Knowledge Principle,LKP)
迪米特法則(Law of Demeter, LoD):一個軟件實體應當儘量少地與其餘實體發生相互做用
若是一個系統符合迪米特法則,那麼當其中某一個模塊發生修改時,就會盡可能少地影響其餘模塊,擴展會相對容易,這是對軟件實體之間通訊的限制,迪米特法則要求限制軟件實體之間通訊的寬度和深度。迪米特法則可下降系統的耦合度,使類與類之間保持鬆散的耦合關係。
迪米特法則還有幾種定義形式,包括:不要和「陌生人」說話、只與你的直接朋友通訊等,在迪米特法則中,對於一個對象,其朋友包括如下幾類:
當前對象自己(this);
以參數形式傳入到當前對象方法中的對象;
當前對象的成員對象;
若是當前對象的成員對象是一個集合,那麼集合中的元素也都是朋友;
當前對象所建立的對象。
任何一個對象,若是知足上面的條件之一,就是當前對象的「朋友」,不然就是「陌生人」。在應用迪米特法則時,一個對象只能與直接朋友發生交互,不要與「陌生人」發生直接交互,這樣作能夠下降系統的耦合度,一個對象的改變不會給太多其餘對象帶來影響。
迪米特法則要求咱們在設計系統時,應該儘可能減小對象之間的交互,若是兩個對象之間沒必要彼此直接通訊,那麼這兩個對象就不該當發生任何直接的相互做用,若是其中的一個對象須要調用另外一個對象的某一個方法的話,能夠經過第三者轉發這個調用。簡言之,就是經過引入一個合理的第三者來下降現有對象之間的耦合度。
在將迪米特法則運用到系統設計中時,要注意下面的幾點:
這 7 種設計原則是軟件設計模式必須儘可能遵循的原則,各類原則要求的側重點不一樣。
整體來講,設計模式按照功能分爲三類23種:
參考:《大話設計模式》、《設計模式之禪》、網上相關設計模式文章