一、設計原則意義
為了設計出一個好的軟體系統。我們必須遵照一定的規則。
衡量軟體設計品質的首要标準是該設計是否能滿足軟體的功能需求。除了功能需求以外,還有很多衡量軟體設計品質的标準,包括可讀性、可複用性、可擴充性、可維護性等。
1、一般一個好的軟體具有以下特點:
可讀性:軟體的設計文檔是否輕易被其他程式員了解。可讀性差的設計會給大型軟體的開發和維護過程帶來嚴重的危害。
可複用性:軟體系統的架構、類、元件等單元能否很容易被本項目的其它部分或者其它項目複用。
可擴充性:軟體面對需求變化時,功能或性能擴充的難易程度。
可維護性:軟體維護(主要是指軟體錯誤的修改、遺漏功能的添加等)的難易程度。
2、内聚度和耦合度标準:
内聚度:
表示一個應用程式的單個單元所負責的任務數量和多樣性。内聚與單個類或者單個方法單元相關。好的軟體設計應該做到高内聚。
理想狀态下,一個代碼單元應該負責一個内聚的任務,也就是說一個任務可以看作是一個邏輯單元。一個方法應該實作一個邏輯操作,一個類應該代表一種類型的實體。
内聚原則背後的主要原因是重用:如果一個方法或一個類隻負責一個定義明确的事情,那麼在不同的上下文環境中,它就能更好地被再次使用。
遵循該規則的另一個優點是,當一個應用程式的某些方面需要做出改變時,我們能夠在相同單元中找到所有相關的部分。
如果一個系統單元隻負責一件事情,就說明這個系統單元有很高的内聚度;如果一個系統單元負責了很多不相關的事情,則說明這個系統單元是内聚度很低。内聚度很高的系統單元通常很容易了解,很容易被複用、擴充和維護。
如果一個方法可以用簡單的“動詞+名詞”的形式來命名(例如,loadFile()、getName()),或者如果一個類可以用準确的名詞來命名(例如,Employee、Student),那麼這樣的類或者方法就是内聚度較高的系統單元;反之,如果類或者方法的名字必須包含“和”、“或”等字樣才能準确反映其功能特性的話,這些類或方法的内聚度就一定不高。
耦合度:
耦合度表示類之間關系的緊密程度。耦合度決定了變更一個應用程式的容易程度。在緊密耦合的類結構中,更改一個類會導緻其它的類也随之需要做出修改。顯然,這是我們在類設計時應該避免的,因為微小的修改會迅速波動影響到整個應用程式。此外,找到需要修改的所有的地方是必須的,實際上就使得修改變得困難并且耗費時間。而在松散耦合的系統中,我們可以更改一個類,不需要修改其它類,而應用程式仍然能夠正常工作。
概括起來,較低的耦合度和較高的内聚度,也即我們常說的“高内聚、低耦合”是所有優秀軟體的共同特征。
如果一個軟體的内聚度和耦合度都符合要求,它也就自然具備了比較好的複用性、可擴充性和可維護性。
二、7大設計原則
1、單一職責原則
單一職責原則(Single Responsibility Principle,SRP)是指:所有的對象都應該有單一的職責,它提供的所有的服務也都僅圍繞着這個職責。換句話說就是:一個類而言,應該僅有一個引起它變化的原因,永遠不要讓一個類存在多個改變的理由。
類的職責是由該類的對象在系統中的角色所決定的。
一個類如果有多個職責,也有多個改變它的理由。反之,如果你能想到一個類存在多個使其改變的原因,那麼這個類就存在多個職責。
2、開閉原則
開閉原則(Open-Close Principle,簡稱OCP)是指一個軟體實體(類、子產品、方法等)應該對擴充開放,對修改關閉。
遵循開閉原則設計出來的子產品具有兩個基本特征:
對于擴充是開放的(Open for extension):子產品的行為可以擴充,當應用的需求改變時,可以對子產品進行擴充,以滿足新的需求。
對于更改是封閉的(Closed for modification):對子產品行為擴充時,不必改動子產品的源代碼或二進制代碼。
這兩個特征看起來是互相沖突的。擴充子產品的行為通常需要修改該子產品的源代碼,而不允許修改的子產品通常被認為是具有固定的行為。
實作開閉原則的關鍵在于抽象化。在Java中,抽象化的具體實作就是使用抽象類或接口。
2.1使用抽象類
在設計類時,對于擁有共同功能的相似類進行抽象化處理,将公用的功能部分放到抽象類中,而将不同的行為封裝在子類中。這樣,在需要對系統進行功能擴充時,隻需要依據抽象類實作新的子類即可。在擴充子類時,不僅可以擁有抽象類的共有屬性和共有方法,還可以擁有自定義的屬性和方法。
2.2接口
與抽象類不同,接口隻定義實作類應該實作的接口方法,而不實作公有的功能。在現在大多數的軟體開發中,都會為實作類定義接口,這樣在擴充子類時必須實作該接口。如果要改換原有的實作,隻需要改換一個實作類即可。
3、裡氏替換原則(The Liskov Substitution Principle,LSP)
在一個軟體系統中,子類應該能夠完全替換任何父類能夠出現的地方,并且經過替換後,不會讓調用父類的客戶程式從行為上有任何改變。
裡氏替換原則實作了開閉原則中的對擴充開放。實作開閉原則的關鍵步驟是抽象化,父類與子類之間的繼承關系就是一種抽象化的展現。是以,裡氏替換原則是實作抽象化的一種規範。違反裡氏替換原則意味着違反了開閉原則,反之未必。裡氏替換原則是使代碼符合開閉原則的一個重要保證。
一般來說,隻要有可能,就不要從具體類繼承。在一個由繼承關系形成的等級結構中,樹葉節點都應當是具體類,樹枝節點都應該是抽象類或者接口。
裡氏替換原則是使代碼符合開閉原則的一個重要的保證,同時,它展現了:
3.1類的繼承原則
裡氏替換原則常用來檢查兩個類是否為繼承關系。在符合裡氏替換原則的繼承關系中,使用父類代碼的地方,用子類代碼替換後,能夠正确的執行動作處理。換句話說,如果子類替換了父類後,不能夠正确執行動作,那麼他們的繼承關系就是不正确的,應該重新設計它們之間的關系。
3.2動作正确性保證
裡氏替換原則對子類進行了限制,是以在為已存在的類進行擴充,來建立一個新的子類時,符合裡氏替換原則的擴充不會給已有的系統引入新的錯誤。
3.3對類的繼承關系的定義
面向對象的設計關注的是對象的行為,它是使用“行為”來對對象進行分類的,隻有行為一緻的對象才能抽象出一個類來。
我們說類的繼承關系就是一種“is-a”關系,實際上指的是行為上的“is-a”關系,可以把它描述為“表現為,act as”。
正方形在設定長度和寬度這兩個行為上,與長方形顯然是不同的。長方形的行為:設定長方形的長度的時候,它的寬度保持不變,設定寬度的時候,長度保持不變。正方形的行為:設定正方形的長度的時候,寬度随之改變;設定寬度的時候,長度随之改變。是以,如果我們把這種行為加到父類長方形的時候,就導緻了正方形無法繼承這種行為。我們“強行”把正方形從長方形繼承過來,就造成無法達到預期的結果。
3.4設計要依賴于使用者需求和具體環境。
繼承關系要求子類要具有基類全部的行為。這裡的行為是指落在需求範圍内的行為。
這裡我們以另一個了解裡氏替換原則的經典例子“鴕鳥非鳥”來做示例。生物學中對于鳥類的定義是“恒溫動物,卵生,全身披有羽毛,身體呈流線形,有角質的喙,眼在頭的兩側。前肢退化成翼,後肢有鱗狀外皮,有四趾”。從生物學角度來看,鴕鳥肯定是一種鳥,是一種繼承關系。但是根據上一個“正方形非長方形”的例子,鴕鳥和鳥之間的繼承關系又可能不成立。那麼,鴕鳥和鳥之間到底是不是繼承關系如何判斷呢?這需要根據使用者需求來判斷。
現在鳥類有四個對外的行為,其中兩個行為分别落在A和B系統需求中,如下圖所示。
A需求期望鳥類提供與飛翔有關的行為,即使鴕鳥跟普通的鳥在外觀上就是100%的相像,但在A需求範圍内,鴕鳥在飛翔這一點上跟其它普通的鳥是不一緻的,它沒有這個能力,是以,鴕鳥類無法從鳥類派生,鴕鳥不是鳥。
B需求期望鳥類提供與羽毛有關的行為,那麼鴕鳥在這一點上跟其它普通的鳥一緻的。雖然它不會飛,但是這一點不在B需求範圍内,是以,它具備了鳥類全部的行為特征,鴕鳥類就能夠從鳥類派生,鴕鳥就是鳥。
所有子類的行為功能必須和使用者對其父類的期望保持一緻,如果子類達不到這一點,那麼必然違反裡氏替換原則。
4、依賴倒轉原則
依賴倒轉原則(Dependency Inversion Principle,簡稱DIP)是指将兩個子產品之間的依賴關系倒置為依賴抽象類或接口。具體有兩層含義:
4.1高層子產品不應該依賴于低層子產品,二者都應該依賴于抽象
4.2 抽象不應該依賴于細節,細節應該依賴于抽象
具體耦合關系:發生在兩個具體的(可執行個體化的)類之間,經由一個類對另一個具體類的直接引用造成。
抽象耦合關系:發生在一個具體類和一個抽象類(或接口)之間,使兩個必須發生關系的類之間存有最大的靈活性。
面向接口程式設計:
在高層子產品和低層子產品之間定義一個抽象接口,高層子產品調用抽象接口定義的方法,低層子產品實作該接口。
依賴倒轉原則的本質就是要求将類之間的關系建立在抽象接口的基礎上的。通過上面的方式,将錯誤的依賴關系倒轉過來,使具體實作類依賴于抽象類和接口。這就是依賴倒轉原則中“倒轉”的由來。
以抽象方式耦合是依賴倒轉原則的關鍵。
5、組合/聚合複用原則
組合/聚合複用原則(Composite/Aggregation Reuse Principle,CARP)是指要盡量使用組合/聚合而非繼承來達到複用目的。另一種解釋是在一個新的對象中使用一些已有的對象,使之成為新對象的一部分;新的對象通過向這些對象委托功能達到複用這些對象的目的。
5.1組合/聚合複用
我們知道組合/聚合都是關聯關系的特殊種類,二者都是展現整體與部分的關系,也就是兩個類之間的是“has-a”關系,它表示某一個角色具有某一項責任。由于組合/聚合都可以将已有的對象加入到新對象中,使之成為新對象的一部分,是以新對象可以調用已有對象的功能,進而實作對象複用。
使用組合/聚合實作複用有如下好處:
5.1.1新對象存取成分對象的唯一方法是通過成分對象的接口。
5.1.2這種複用是黑箱複用,因為成分對象的内部細節是新對象所看不見的。
5.1.3這種複用所需的依賴較少。
5.1.4每一個新的類可以将焦點集中在一個任務上。
5.1.5這種複用可以在運作時間内動态進行,作為整體的新對象可以動态地引用與部分對象類型相同的對象。也就是說,組合/聚合是動态行為,即運作時行為。可以通過使用組合/聚合的方式在設計上獲得更高的靈活性。
當然,這種複用也有缺點。其中最主要的缺點就是系統中會有較多的對象需要管理。
一般來說,如果一個角色得到了更多的責任,就可以使用組合/聚合關系将新的責任委派到合适的對象上。
5.2繼承複用
繼承是面向對象語言特有的複用工具。由于使用繼承關系時,新的實作較為容易,因父類的大部分功能可以通過繼承的關系自動進入子類;同時,修改和擴充繼承而來的實作較為容易。于是,在面向對象設計理論的早期,程式設計師十分熱衷于繼承,好像繼承就是最好的複用手段,于是繼承也成為了最容易被濫用的複用工具。然而,繼承有多個缺點:
5.2.1繼承複用破壞封裝,因為繼承将父類的實作細節暴露給子類。由于父類的内部細節常常是對于子類透明的,是以這種複用是透明的複用,又稱“白箱”複用。
5.2.2如果父類發生改變,那麼子類的實作也不得不發生改變。
5.2.3從父類繼承而來的實作是靜态的,也就是編譯時行為,不可能在運作時間内發生改變,沒有足夠的靈活性。
正是因為繼承有上述缺點,是以應首先使用組合/聚合,其次才考慮繼承,達到複用的目的。并且在使用繼承時,要嚴格遵循裡氏替換原則。
要正确的選擇組合/聚合和繼承,必須透徹的了解裡氏替換原則和Coad法則。裡氏替換原則前面學習過,Coad法則由Peter Coad提出,總結了一些什麼時候使用繼承作為複用工具的條件。
5.3Coad條件全部被滿足時,才應當使用繼承關系
5.3.1子類是父類的一個特殊種類,而不是父類的一個角色,也就是區分“has-a”和“is-a”。隻有“is-a”關系才符合繼承關系,“has-a”關系應當用組合/聚合來描述。
5.3.2永遠不會出現需要将子類換成另外一個類的子類的情況。如果不能肯定将來是否會變成另外一個子類的話,就不要使用繼承。
5.3.3子類具有擴充父類的責任,而不是具有置換(重寫)或登出掉父類的責任。如果一個子類需要大量的置換掉父類的行為,那麼這個類就不應該是這個父類的子類。
5.3.4隻有在分類學角度上有意義時,才可以使用繼承。不要從工具類繼承。
6、接口隔離原則
接口隔離原則(Interface Segregation Principle,簡稱ISP)是指客戶不應該依賴它們用不到的方法,隻給每個客戶它所需要的接口。換句話說,就是不能強迫使用者去依賴那些他們不使用的接口。
接口隔離原則實際上包含了兩層意思:
6.1接口的設計原則
接口的設計應該遵循最小接口原則,不要把使用者不使用的方法塞進同一個接口裡。如果一個接口的方法沒有被使用到,則說明該接口過胖,應該将其分割成幾個功能專一的接口,使用多個專門的接口比使用單一的總接口要好。
6.2接口的繼承原則
如果一個接口A繼承另一個接口B,則接口A相當于繼承了接口B的方法,那麼繼承了接口B後的接口A也應該遵循上述原則:不應該包含使用者不使用的方法。反之,則說明接口A被B給污染了,應該重新設計它們的關系。
6.3通過多重繼承分離接口
多重繼承可以有兩個方式,第一種方式是同時實作兩個接口,屬于多重接口繼承;第二種方式是實作一個接口,同時繼承一個具體類,實際上也是一種多重繼承。
6.4通過委托分離接口
使用聚合群組合來将部分實作交給已知類實作。自己在實作部分接口。
7、迪米特法則
迪米特法則(Law of Demeter,簡稱LOD),又稱為“最少知識原則”,它的定義為:一個軟體實體應當盡可能少的與其他實體發生互相作用。這樣,當一個子產品修改時,就會盡量少的影響其他的子產品,擴充會相對容易。迪米特法則是對軟體實體之間通信的限制,它對軟體實體之間通信的寬度和深度做出了要求。迪米特的其它表述方式為:
7.1隻與你直接的朋友們通信。
7.2不要跟“陌生人”說話。
7.3每一個軟體機關對其他的機關都隻有最少的知識,而且局限于那些與本機關密切相關的軟體機關。
7.4做為“朋友”的條件為:
7.4.1目前對象本身(this);
7.4.2被當做目前對象的方法的參數傳入進來的對象;
7.4.3目前對象的方法所建立或者執行個體化的任何對象;
7.4.4目前對象的任何元件(被目前對象的執行個體變量引用的任何對象)。
出處:http://www.uml.org.cn/mxdx/201409044.asp