1. 程式人生 > 程式設計 >設計模式--7種面向物件設計原則

設計模式--7種面向物件設計原則

軟體設計模式的產生背景

1995 年,艾瑞克·伽馬(ErichGamma)、理査德·海爾姆(Richard Helm)、拉爾夫·約翰森(Ralph Johnson)、約翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《設計模式:可複用面向物件軟體的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software)一書,這是設計模式領域裡程碑的事件,導致了軟體設計模式的突破。這 4 位作者在軟體開發領域裡也以他們的“四人組”(Gang of Four,GoF)匿名著稱。

軟體設計模式的概念與意義

有關軟體設計模式的定義很多,有些從模式的特點來說明,有些從模式的作用來說明。本教程給出的定義是大多數學者公認的,從以下兩個方面來說明。

1. 軟體設計模式的概念

軟體設計模式(Software Design Pattern),又稱設計模式,是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。它描述了在軟體設計過程中的一些不斷重複發生的問題,以及該問題的解決方案。也就是說,它是解決特定問題的一系列套路,是前輩們的程式碼設計經驗的總結,具有一定的普遍性,可以反覆使用。其目的是為了提高程式碼的可重用性、程式碼的可讀性和程式碼的可靠性。

2. 學習設計模式的意義

設計模式的本質是面向物件設計原則的實際運用,是對類的封裝性、繼承性和多型性以及類的關聯關係和組合關係的充分理解。正確使用設計模式具有以下優點:

  • 可以提高程式設計師的思維能力、程式設計能力和設計能力。

  • 使程式設計更加標準化、程式碼編制更加工程化,使軟體開發效率大大提高,從而縮短軟體的開發週期。

  • 使設計的程式碼可重用性高、可讀性強、可靠性高、靈活性好、可維護性強。

當然,軟體設計模式只是一個引導。在具體的軟體開發中,必須根據設計的應用系統的特點和要求來恰當選擇。對於簡單的程式開發,可能寫一個簡單的演演算法要比引入某種設計模式更加容易。但對大專案的開發或者框架設計,用設計模式來組織程式碼顯然更好。

軟體設計模式的基本要素

軟體設計模式使人們可以更加簡單方便地複用成功的設計和體系結構,它通常包含以下幾個基本要素:模式名稱、別名、動機、問題、解決方案、效果、結構、模式角色、合作關係、實現方法、適用性、已知應用、例程、模式擴充套件和相關模式等,其中最關鍵的元素包括以下 4 個主要部分。

1. 模式名稱

每一個模式都有自己的名字,通常用一兩個詞來描述,可以根據模式的問題、特點、解決方案、功能和效果來命名。模式名稱(PatternName)有助於我們理解和記憶該模式,也方便我們來討論自己的設計。

2. 問題

問題(Problem)描述了該模式的應用環境,即何時使用該模式。它解釋了設計問題和問題存在的前因後果,以及必須滿足的一系列先決條件。

3. 解決方案

模式問題的解決方案(Solution)包括設計的組成成分、它們之間的相互關係及各自的職責和協作方式。因為模式就像一個模板,可應用於多種不同場合,所以解決方案並不描述一個特定而具體的設計或實現,而是提供設計問題的抽象描述和怎樣用一個具有一般意義的元素組合(類或物件的 組合)來解決這個問題。

4. 效果

描述了模式的應用效果以及使用該模式應該權衡的問題,即模式的優缺點。主要是對時間和空間的衡量,以及該模式對系統的靈活性、擴充性、可移植性的影響,也考慮其實現問題。顯式地列出這些效果(Consequence)對理解和評價這些模式有很大的幫助。

七種面向物件設計原則

在軟體開發中,為了提高軟體系統的可維護性和可複用性,增加軟體的可擴充套件性和靈活性,程式設計師要儘量根據 7 條原則來開發程式,從而提高軟體開發效率、節約軟體開發成本和維護成本。我們將在下面的依次來介紹這 7 條原則。

(一)開閉原則

1. 開閉原則的定義

開閉原則(Open Closed Principle,OCP)由勃蘭特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向物件軟體構造》(Object Oriented Software Construction)中提出:軟體實體(包括專案中劃分出的模組、類與介面和方法)應當對擴充套件開放,對修改關閉(Software entities should be open for extension,but closed for modification),這就是開閉原則的經典定義。

其含義是:當應用的需求改變時,在不修改軟體實體的原始碼或者二進位製程式碼的前提下,可以擴充套件模組的功能,使其滿足新的需求。

2. 開閉原則的作用

開閉原則是面向物件程式設計的終極目標,它使軟體實體擁有一定的適應性和靈活性的同時具備穩定性和延續性。具體來說,其作用如下。

  • 對軟體測試的影響

軟體遵守開閉原則的話,軟體測試時只需要對擴充套件的程式碼進行測試就可以了,因為原有的測試程式碼仍然能夠正常執行。

  • 可以提高程式碼的可複用性

粒度越小,被複用的可能性就越大;在面向物件的程式設計中,根據原子和抽象程式設計可以提高程式碼的可複用性。

  • 可以提高軟體的可維護性

遵守開閉原則的軟體,其穩定性高和延續性強,從而易於擴充套件和維護。

3. 開閉原則的實現方法

可以通過“抽象約束、封裝變化”來實現開閉原則,即通過介面或者抽象類為軟體實體定義一個相對穩定的抽象層,而將相同的可變因素封裝在相同的具體實現類中。

因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟體架構的穩定。而軟體中易變的細節可以從抽象派生來的實現類來進行擴充套件,當軟體需要發生變化時,只需要根據需求重新派生一個實現類來擴充套件就可以了。

(二)里氏替換原則

1. 里氏替換原則的定義

里氏替換原則(Liskov Substitution Principle,LSP)由麻省理工學院電腦科學實驗室的里斯科夫(Liskov)女士在 1987 年的“面向物件技術的高峰會議”(OOPSLA)上發表的一篇文章《資料抽象和層次》(Data Abstraction and Hierarchy)裡提出來的,她提出:繼承必須確保超類所擁有的性質在子類中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

里氏替換原則主要闡述了有關繼承的一些原則,也就是什麼時候應該使用繼承,什麼時候不應該使用繼承,以及其中蘊含的原理。里氏替換原是繼承複用的基礎,它反映了基類與子類之間的關係,是對開閉原則的補充,是對實現抽象化的具體步驟的規範。

2. 里氏替換原則的作用

里氏替換原則的主要作用如下。

  • 里氏替換原則是實現開閉原則的重要方式之一。

  • 它克服了繼承中重寫父類造成的可複用性變差的缺點。

  • 它是動作正確性的保證。即類的擴充套件不會給已有的系統引入新的錯誤,降低了程式碼出錯的可能性。

3. 里氏替換原則的實現方法

里氏替換原則通俗來講就是:子類可以擴充套件父類的功能,但不能改變父類原有的功能。也就是說:子類繼承父類時,除新增新的方法完成新增功能外,儘量不要重寫父類的方法。

如果通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可複用性會比較差,特別是運用多型比較頻繁時,程式執行出錯的概率會非常大。

如果程式違背了里氏替換原則,則繼承類的物件在基類出現的地方會出現執行錯誤。這時其修正方法是:取消原來的繼承關係,重新設計它們之間的關係。
關於里氏替換原則的例子,最有名的是“正方形不是長方形”。當然,生活中也有很多類似的例子,例如,企鵝、鴕鳥和幾維鳥從生物學的角度來劃分,它們屬於鳥類;但從類的繼承關係來看,由於它們不能繼承“鳥”會飛的功能,所以它們不能定義成“鳥”的子類。

(三)依賴倒置原則

1. 依賴倒置原則的定義

依賴倒置原則(Dependence Inversion Principle,DIP)是 Object Mentor 公司總裁羅伯特·馬丁(Robert C.Martin)於 1996 年在 C++ Report 上發表的文章。
依賴倒置原則的原始定義為:高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向介面程式設計,不要面向實現程式設計。
依賴倒置原則是實現開閉原則的重要途徑之一,它降低了客戶與實現模組之間的耦合。
由於在軟體設計中,細節具有多變性,而抽象層則相對穩定,因此以抽象為基礎搭建起來的架構要比以細節為基礎搭建起來的架構要穩定得多。這裡的抽象指的是介面或者抽象類,而細節是指具體的實現類。
使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給它們的實現類去完成。

2. 依賴倒置原則的作用

依賴倒置原則的主要作用如下:

  • 依賴倒置原則可以降低類間的耦合性。

  • 依賴倒置原則可以提高系統的穩定性。

  • 依賴倒置原則可以減少並行開發引起的風險。

  • 依賴倒置原則可以提高程式碼的可讀性和可維護性。

3. 依賴倒置原則的實現方法

依賴倒置原則的目的是通過要面向介面的程式設計來降低類間的耦合性,所以我們在實際程式設計中只要遵循以下4點,就能在專案中滿足這個規則。

  • 每個類儘量提供介面或抽象類,或者兩者都具備。

  • 變數的宣告型別儘量是介面或者是抽象類。

  • 任何類都不應該從具體類派生。

  • 使用繼承時儘量遵循里氏替換原則。

(四)單一職責原則

1. 單一職責原則的定義

單一職責原則(Single Responsibility Principle,SRP)又稱單一功能原則,由羅伯特·C.馬丁(Robert C. Martin)於《敏捷軟體開發:原則、模式和實踐》一書中提出的。這裡的職責是指類變化的原因,單一職責原則規定一個類應該有且僅有一個引起它變化的原因,否則類應該被拆分(There should never be more than one reason for a class to change)。

該原則提出物件不應該承擔太多職責,如果一個物件承擔了太多的職責,至少存在以下兩個缺點:

  • 一個職責的變化可能會削弱或者抑制這個類實現其他職責的能力;

  • 當客戶端需要該物件的某一個職責時,不得不將其他不需要的職責全都包含進來,從而造成冗餘程式碼或程式碼的浪費。

2. 單一職責原則的作用

單一職責原則的核心就是控制類的粒度大小、將物件解耦、提高其內聚性。如果遵循單一職責原則將有以下優點:

  • 降低類的複雜度。一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單得多。

  • 提高類的可讀性。複雜性降低,自然其可讀性會提高。

  • 提高系統的可維護性。可讀性提高,那自然更容易維護了。

  • 變更引起的風險降低。變更是必然的,如果單一職責原則遵守得好,當修改一個功能時,可以顯著降低對其他功能的影響。

3. 單一職責原則的實現方法

單一職責原則是最簡單但又最難運用的原則,需要設計人員發現類的不同職責並將其分離,再封裝到不同的類或模組中。而發現類的多重職責需要設計人員具有較強的分析設計能力和相關重構經驗。

(五)介面隔離原則

1. 介面隔離原則的定義

介面隔離原則(Interface Segregation Principle,ISP)要求程式設計師儘量將臃腫龐大的介面拆分成更小的和更具體的介面,讓介面中只包含客戶感興趣的方法。

2002 年羅伯特·C.馬丁給“介面隔離原則”的定義是:客戶端不應該被迫依賴於它不使用的方法(Clients should not be forced to depend on methods they do not use)。該原則還有另外一個定義:一個類對另一個類的依賴應該建立在最小的介面上(The dependency of one class to another one should depend on the smallest possible interface)。

以上兩個定義的含義是:要為各個類建立它們需要的專用介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。

介面隔離原則和單一職責都是為了提高類的內聚性、降低它們之間的耦合性,體現了封裝的思想,但兩者是不同的:

  • 單一職責原則注重的是職責,而介面隔離原則注重的是對介面依賴的隔離。

  • 單一職責原則主要是約束類,它針對的是程式中的實現和細節;介面隔離原則主要約束介面,主要針對抽象和程式整體框架的構建。

2. 介面隔離原則的作用

介面隔離原則是為了約束介面、降低類對介面的依賴性,遵循介面隔離原則有以下 5 個優點。

  1. 將臃腫龐大的介面分解為多個粒度小的介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

  2. 介面隔離提高了系統的內聚性,減少了對外互動,降低了系統的耦合性。

  3. 如果介面的粒度大小定義合理,能夠保證系統的穩定性;但是,如果定義過小,則會造成介面數量過多,使設計複雜化;如果定義太大,靈活性降低,無法提供定製服務,給整體專案帶來無法預料的風險。

  4. 使用多個專門的介面還能夠體現物件的層次,因為可以通過介面的繼承,實現對總介面的定義。

  5. 能減少專案工程中的程式碼冗餘。過大的大介面裡面通常放置許多不用的方法,當實現這個介面的時候,被迫設計冗餘的程式碼。

3. 介面隔離原則的實現方法

在具體應用介面隔離原則時,應該根據以下幾個規則來衡量:

  • 介面儘量小,但是要有限度。一個介面只服務於一個子模組或業務邏輯。

  • 為依賴介面的類定製服務。只提供呼叫者需要的方法,遮蔽不需要的方法。

  • 瞭解環境,拒絕盲從。每個專案或產品都有選定的環境因素,環境不同,介面拆分的標準就不同深入瞭解業務邏輯。

  • 提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。

(六)迪米特法則

1. 迪米特法則的定義

迪米特法則(Law of Demeter,LoD)又叫作最少知識原則(Least Knowledge Principle,LKP),產生於 1987 年美國東北大學(Northeastern University)的一個名為迪米特(Demeter)的研究專案,由伊恩·荷蘭(Ian Holland)提出,被 UML 創始者之一的布奇(Booch)普及,後來又因為在經典著作《程式設計師修煉之道》(The Pragmatic Programmer)提及而廣為人知。

迪米特法則的定義是:只與你的直接朋友交談,不跟“陌生人”說話(Talk only to your immediate friends and not to strangers)。其含義是:如果兩個軟體實體無須直接通訊,那麼就不應當發生直接的相互呼叫,可以通過第三方轉發該呼叫。其目的是降低類之間的耦合度,提高模組的相對獨立性。

迪米特法則中的“朋友”是指:當前物件本身、當前物件的成員物件、當前物件所建立的物件、當前物件的方法引數等,這些物件同當前物件存在關聯、聚合或組合關係,可以直接訪問這些物件的方法。

2. 迪米特法則的優點

迪米特法則要求限制軟體實體之間通訊的寬度和深度,正確使用迪米特法則將有以下兩個優點。

  • 降低了類之間的耦合度,提高了模組的相對獨立性。

  • 由於親和度降低,從而提高了類的可複用率和系統的擴充套件性。


但是,過度使用迪米特法則會使系統產生大量的中介類,從而增加系統的複雜性,使模組之間的通訊效率降低。所以,在釆用迪米特法則時需要反覆權衡,確保高內聚和低耦合的同時,保證系統的結構清晰。

3. 迪米特法則的實現方法

從迪米特法則的定義和特點可知,它強調以下兩點:

  • 從依賴者的角度來說,只依賴應該依賴的物件。

  • 從被依賴者的角度說,只暴露應該暴露的方法。


所以,在運用迪米特法則時要注意以下 6 點:

  • 在類的劃分上,應該建立弱耦合的類。類與類之間的耦合越弱,就越有利於實現可複用的目標。

  • 在類的結構設計上,儘量降低類成員的訪問許可權。

  • 在類的設計上,優先考慮將一個類設定成不變類。

  • 在對其他類的引用上,將引用其他物件的次數降到最低。

  • 不暴露類的屬性成員,而應該提供相應的訪問器(set 和 get 方法)。

  • 謹慎使用序列化(Serializable)功能。

(七)合成複用原則

1. 合成複用原則的定義

合成複用原則(Composite Reuse Principle,CRP)又叫組合/聚合複用原則(Composition/Aggregate Reuse Principle,CARP)。它要求在軟體複用時,要儘量先使用組合或者聚合等關聯關係來實現,其次才考慮使用繼承關係來實現。

如果要使用繼承關係,則必須嚴格遵循里氏替換原則。合成複用原則同里氏替換原則相輔相成的,兩者都是開閉原則的具體實現規範。

2. 合成複用原則的重要性

通常類的複用分為繼承複用和合成複用兩種,繼承複用雖然有簡單和易實現的優點,但它也存在以下缺點:

  • 繼承複用破壞了類的封裝性。因為繼承會將父類的實現細節暴露給子類,父類對子類是透明的,所以這種複用又稱為“白箱”複用。

  • 子類與父類的耦合度高。父類的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴充套件與維護。

  • 它限制了複用的靈活性。從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在執行時不可能發生變化。


採用組合或聚合複用時,可以將已有物件納入新物件中,使之成為新物件的一部分,新物件可以呼叫已有物件的功能,它有以下優點:

  • 它維持了類的封裝性。因為成分物件的內部細節是新物件看不見的,所以這種複用又稱為“黑箱”複用。

  • 新舊類之間的耦合度低。這種複用所需的依賴較少,新物件存取成分物件的唯一方法是通過成分物件的介面。

  • 複用的靈活性高。這種複用可以在執行時動態進行,新物件可以動態地引用與成分物件型別相同的物件。

3. 合成複用原則的實現方法

合成複用原則是通過將已有的物件納入新物件中,作為新物件的成員物件來實現的,新物件可以呼叫已有物件的功能,從而達到複用。

以上我們介紹了七種面向物件設計原則,下一篇將會介紹建立型模式中的單例與多例模式。


感謝閱讀!


喜歡本文的朋友,歡迎關注“isevena