設計模式的基本原則
設計模式的基本原則非常重要,只要真正深入地理解了設計原則,很多設計模式其實就是原則的應用而已,或許在不知不覺中就在使用設計模式了:
單一職責原則(SRP),就一個類而言,應該僅有一個引起它變化的原因。
單一職責原則(SRP:Single responsibility principle)
定義
一個類應該只有一個發生變化的原因,即一個類只負責一項職責。
如果一個類有多個職責,這些職責就耦合在了一起。當一個職責發生變化時,可能會影響其它的職責。另外,多個職責耦合在一起會影響復用性。
此原則的核心是解耦和增強內聚性。
由來
類A負責兩個職責:職責P1,職責P2。當由于職責P1需求發生改變而需要修改類A時,有可能會導致原本運行正常的職責P2功能發生故障。
解決方案
遵循SRP。分別建立兩個類A1、A2,使A1完成職責P1,A2完成職責P2。這樣,當修改類A1時,不會影響到職責A2;同理,當修改A2時,也不會影響到職責P1。
優點
降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多。
提高類的可讀性,提高系統的可維護性。
變更引起的風險降低,變更是必然的,如果SRP遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
開放-封閉原則(OCP),是說軟件實體(類、模塊、函數等等)應該可以拓展,但是不可修改。
??注意這個是理想化的
定義
一個軟件實體(如類、模塊、函數)應當對擴展開放,對修改關閉。
定義解讀
在項目開發的時候,都不能指望需求是確定不變化的,大部分情況下,需求是變化的。那么如何應對需求變化的情況?這就是開放-關閉原則要談的。
開放-封閉原則的思想就是設計的時候,盡量讓設計的類做好后就不再修改,如果有新的需求,通過新加類的方式來滿足,而不去修改現有的類(代碼)。那么在實際的項目開發中,是否能做到絕對的對修改關閉呢?答案一般也是否定的。既然這樣,那么就要求我們在開發前,去找出變化點,然后針對變化點構造抽象,隔離出這些變化。由此可見,實現開閉原則關鍵是抽象。
優點
具有靈活性,通過拓展一個功能模塊即可實現功能的擴充,不需修改內部代碼。
具有穩定性,表現在基本功能類不允許被修改,使得被破壞的程度大大下降。
總結
對于設計模式的六大設計原則,單一職責原則主要說明類的職責要單一;里氏替換原則強調不要破壞繼承體系;依賴倒置原則描述要面向接口編程;接口隔離原則講解設計接口的時候要精簡;迪米特法則告訴我們要降低耦合;開閉原則講述的是對擴展開放,對修改關閉。
六大設計原則并沒有很明顯的界限,當我們在遵守某一個設計原則的時候,可能也遵守了其他的設計原則。設計原則是后面要講述的設計模式的基礎,因此在本系列講述設計模式之前,對設計原則進行了解說。
依賴倒轉原則(DIP),A. 高層模塊不應該依賴低層模塊,兩個都應該依賴抽象。B. 抽象不應該依賴細節,細節應該依賴抽象。
定義
高層模塊不應該依賴于低層模塊,二者都應該依賴于抽象;抽象不應該依賴細節;細節應該依賴抽象。
定義解讀
依賴倒置原則在程序編碼中經常運用,其核心思想就是面向接口編程,高層模塊不應該依賴低層模塊(原子操作的模塊),兩者都應該依賴于抽象。我們平時常說的“針對接口編程,不要針對實現編程”就是依賴倒轉原則的最好體現:接口(也可以是抽象類)就是一種抽象,只要不修改接口聲明,大家可以放心大膽調用,至于接口的內部實現則無需關心,可以隨便重構。這里,接口就是抽象,而接口的實現就是細節。如果不管高層模塊還是底層模塊,它們都依賴于抽象,具體一點就是接口或者抽象類,只要接口是穩定的,那么任何一個的更改都不用擔心其他受到影響,這就使得無論高層模塊還是低層模塊都可以很容易地被復用。
依賴倒轉原則其實可以說是面向對象設計的標志,用哪種語言來編寫程序不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即程序中所有的依賴關系都是終止于抽象類或者接口,那就是面向對象的設計,反之那就是過程化的設計(說這句話可能不怎么好理解,再加上一句話就好理解了:面向對象的設計,出發點就是應對變化的問題)。
再舉一個生活中的例子,電腦中內存或者顯卡插槽,其實是一種接口,而這就是抽象;只要符合這個接口的要求,無論是用金士頓的內存,還是其它的內存,無論是4G的,還是8G的,都可以很方便、輕松的插到電腦上使用。而這些內存條就是具體實現,就是細節。
錯誤做法:抽象A依賴于實現細節b
正確做法:抽象A依賴于抽象B,實現細節b實現抽象B
優點
代碼結構清晰,維護容易。
問題提出
類A直接依賴類B,假如需要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
解決方案
將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。
依賴倒置原則基于這樣一個事實:相對于細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在C#/Java中,抽象指的是接口或者抽象類;在Objective-C中,抽象指的是委托,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規范和契約,而不去涉及任何具體的操作,把展現細節的任務交給它們的實現類去完成
里氏代換原則(LSP),子類型必須能夠替換掉它們的父類型。
定義:
所有引用基類(父類)的地方必須能透明地使用其子類的對象。
只要父類能出現的地方子類就可以出現,而且替換為子類還不產生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。
下面通過具體程序實例進行進一步的解釋。
父類作函數聲明,但并不實現具體函數,以虛函數的形式呈現,即什么也不做,空實現。
子類一繼承自父類SourceView,并具體實現 show 方法
子類二同樣繼承自父類SourceView,并具體實現 show 方法
最后在ViewController中引入,并實例化,最終運行結果如下:
由此,便用一個非常簡單的實例演示了里氏代換原則。
里氏代換原則是實現開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進行定義,而在運行時再確定其子類類型,用子類對象來替換父類對象。
在使用里氏代換原則時需要注意如下幾個問題:
子類必須實現父類所有非私有的屬性和方法 或 子類的所有非私有屬性和方法必須在父類中聲明。即,子類可以有自己的“個性”,這也就是說,里氏代換原則可以正著用,不能反著用(在項目中,采用里氏替換原則時,盡量避免子類的“個性”,一旦子類有“個性”,這個子類和父類之間的關系就很難調和了)。根據里氏代換原則,為了保證系統的擴展性,在程序中通常使用父類來進行定義,如果一個方法只存在子類中,在父類中不提供相應的聲明,則無法在以父類定義的對象中使用該方法。
盡量把父類設計為抽象類或者接口,讓子類繼承父類或實現父接口,并實現在父類中聲明的方法,運行時,子類實例替換父類實例,我們可以很方便地擴展系統的功能,同時無須修改原有子類的代碼,增加新的功能可以通過增加一個新的子類來實現。
迪米特法則(LoD),如果兩個類不必彼此直接通信,那么這兩個類就不應當發生直接的相互作用。如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。
迪米特法則又叫最少知道原則,最早是在1987年由美國Northeastern University的Ian Holland提出。類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。于是就提出了迪米特法則。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對于被依賴的類來說,無論邏輯多么復雜,都盡量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。
迪米特法則的簡單定義:
只與直接的朋友通信。
首先來解釋一下什么是直接的朋友:每個對象都會與其他對象有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系。耦合的方式很多,依賴、關聯、組合、聚合等。其中,當前對象本身(this)、成員變量、以參數形式傳入當前對象方法中的對象、方法返回值中的類,當前對象創建的對象為直接的朋友。
迪米特法則的作用:
迪米特法則可降低系統的耦合度,使類與類之間保持較低的耦合關系。我們知道,軟件編程有一個總的原則:低耦合,高內聚。無論是面向過程編程還是面向對象編程,只有使各個模塊之間的耦合盡量的低,才能提高代碼的復用率。低耦合的優點不言而喻,但是怎么樣編程才能做到低耦合呢?那正是迪米特法則要去完成的。
迪米特法則的示例:
如圖1所示,類之間存在復雜的交互關系,一個ClassBase類的動作執行將導致多個其它關聯類產生響應,例如,當ClassBase有動作時時,和它有關聯的類ClassB、ClassC、ClassD等都將發生改變,在初始設計方案中,對象之間的交互關系可簡化為如圖1所示結構:
在圖1中,由于類之間的交互關系復雜,導致在該系統中增加新的對象時需要修改與之交互的其它類的源代碼,系統擴展性較差,也不便于增加和刪除新對象。
在本示例中,可以通過引入一個專門用于控制對象間交互的中介類(Mediator)來降低各對象之間的耦合度。引入中間類之后,相關對象之間不再發生直接引用,而是將請求先轉發給中間類,再由中間類來完成對其它對象的調用。當需要增加或刪除新的對象時,只需修改中間類即可,無須修改新增對象或已有對象的源代碼,重構后結構如圖2所示:
迪米特法則的使用總結:
(1).在類的劃分上,應當盡量創建松耦合的類,類之間的耦合度越低,就越有利于復用,一個處在松耦合中的類一旦被修改,不會對關聯的類造成太大波及;
(2).在類的結構設計上,每一個類都應當盡量降低其成員變量和成員函數的訪問權限;
(3).在類的設計上,只要有可能,一個類型應當設計成不變類;
(4).在對其他類的引用上,一個對象對其他對象的引用應當降到最低。
合成/聚合復用原則(CARP),盡量使用合成/聚合,盡量不要使用類繼承。
組合/聚合復用原則(Composite/Aggregate Reuse Principle CARP).組合和聚合都是對象建模中關聯(Association)關系的一種.聚合表示整體與部分的關系,表示“含有”,整體由部分組合而成,部分可以脫離整體作為一個獨立的個體存在。組合則是一種更強的聚合,部分組成整體,而且不可分割,部分不能脫離整體而單獨存在。在合成關系中,部分和整體的生命周期一樣,組合的新的對象完全支配其組成部分,包括他們的創建和銷毀。一個合成關系中成分對象是不能與另外一個合成關系共享。
組合/聚合和繼承是實現復用的兩個基本途徑。合成復用原則是指盡量使用合成/聚合,而不是使用繼承。
只有當以下的條件全部被滿足時,才應當使用繼承關系。
1. 子類是超類的一個特殊種類,而不是超類的一個角色,也就是區分“Has-A”和“Is-A”.只有“Is-A”關系才符合繼承關系,“Has-A”關系應當使用聚合來描述。
2 .永遠不會出現需要將子類換成另外一個類的子類的情況。如果不能肯定將來是否會變成另外一個子類的話,就不要使用繼承。
3 .子類具有擴展超類的責任,而不是具有置換掉或注銷掉超類的責任。如果一個子類需要大量的置換掉超類的行為,那么這個類就不應該是這個超類的子類。
錯誤的使用繼承而不是合成/聚合的一個常見原因是錯誤地把“Has-A”當成了“Is-A”.”Is-A”代表一個類是另外一個類的一種;而“Has-A”代表一個類是另外一個類的一個角色,而不是另外一個類的特殊種類。
我們需要辦理一張銀行卡,如果銀行卡默認都擁有了存款、取款和透支的功能,那么我們辦理的卡都將具有這個功能,此時使用了繼承關系:
為了靈活地擁有各種功能,此時可以分別設立儲蓄卡和信用卡兩種,并有銀行卡來對它們進行聚合使用。此時采用了合成復用原則: