23種設(shè)計(jì)模式之七大設(shè)計(jì)原則
一、前言
1.1 產(chǎn)生背景
設(shè)計(jì)模式”這個(gè)術(shù)語(yǔ)最初并不是出現(xiàn)在軟件設(shè)計(jì)中,而是被用于建筑領(lǐng)域的設(shè)計(jì)中。
1995 年,艾瑞克·伽馬(ErichGamma)、理査德·海爾姆(Richard Helm)、拉爾夫·約翰森(Ralph Johnson)、約翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》(Design Patterns: Elements of Reusable Object-Oriented Software)一書(shū),在本教程中收錄了 23 個(gè)設(shè)計(jì)模式,這是設(shè)計(jì)模式領(lǐng)域里程碑的事件,導(dǎo)致了軟件設(shè)計(jì)模式的突破。這 4 位作者在軟件開(kāi)發(fā)領(lǐng)域里也以他們的“四人組”(Gang of Four,GoF)匿名著稱。
直到今天,狹義的設(shè)計(jì)模式還是本教程中所介紹的 23 種經(jīng)典設(shè)計(jì)模式。
1.2 軟件設(shè)計(jì)模式的概念
軟件設(shè)計(jì)模式(Software Design Pattern),又稱設(shè)計(jì)模式,是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過(guò)分類(lèi)編目的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。它描述了在軟件設(shè)計(jì)過(guò)程中的一些不斷重復(fù)發(fā)生的問(wèn)題,以及該問(wèn)題的解決方案。也就是說(shuō),它是解決特定問(wèn)題的一系列套路,是前輩們的代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié),具有一定的普遍性,可以反復(fù)使用。其目的是為了提高代碼的可重用性、代碼的可讀性和代碼的可靠性。
1.3 學(xué)習(xí)設(shè)計(jì)模式的意義
設(shè)計(jì)模式的本質(zhì)是面向?qū)ο笤O(shè)計(jì)原則的實(shí)際運(yùn)用,是對(duì)類(lèi)的封裝性、繼承性和多態(tài)性以及類(lèi)的關(guān)聯(lián)關(guān)系和組合關(guān)系的充分理解。正確使用設(shè)計(jì)模式具有以下優(yōu)點(diǎn)。
1.4 軟件設(shè)計(jì)模式的基本要素
軟件設(shè)計(jì)模式使人們可以更加簡(jiǎn)單方便地復(fù)用成功的設(shè)計(jì)和體系結(jié)構(gòu),它通常包含以下幾個(gè)基本要素:模式名稱、別名、動(dòng)機(jī)、問(wèn)題、解決方案、效果、結(jié)構(gòu)、模式角色、合作關(guān)系、實(shí)現(xiàn)方法、適用性、已知應(yīng)用、例程、模式擴(kuò)展和相關(guān)模式等,其中最關(guān)鍵的元素包括以下 4 個(gè)主要部分。
模式名稱:每一個(gè)模式都有自己的名字,通常用一兩個(gè)詞來(lái)描述,可以根據(jù)模式的問(wèn)題、特點(diǎn)、解決方案、功能和效果來(lái)命名。模式名稱(PatternName)有助于我們理解和記憶該模式,也方便我們來(lái)討論自己的設(shè)計(jì)。
問(wèn)題:?jiǎn)栴}(Problem)描述了該模式的應(yīng)用環(huán)境,即何時(shí)使用該模式。它解釋了設(shè)計(jì)問(wèn)題和問(wèn)題存在的前因后果,以及必須滿足的一系列先決條件。
解決方案:模式問(wèn)題的解決方案(Solution)包括設(shè)計(jì)的組成成分、它們之間的相互關(guān)系及各自的職責(zé)和協(xié)作方式。因?yàn)槟J骄拖褚粋€(gè)模板,可應(yīng)用于多種不同場(chǎng)合,所以解決方案并不描述一個(gè)特定而具體的設(shè)計(jì)或?qū)崿F(xiàn),而是提供設(shè)計(jì)問(wèn)題的抽象描述和怎樣用一個(gè)具有一般意義的元素組合(類(lèi)或?qū)ο蟮?組合)來(lái)解決這個(gè)問(wèn)題。
效果:描述了模式的應(yīng)用效果以及使用該模式應(yīng)該權(quán)衡的問(wèn)題,即模式的優(yōu)缺點(diǎn)。主要是對(duì)時(shí)間和空間的衡量,以及該模式對(duì)系統(tǒng)的靈活性、擴(kuò)充性、可移植性的影響,也考慮其實(shí)現(xiàn)問(wèn)題。顯式地列出這些效果(Consequence)對(duì)理解和評(píng)價(jià)這些模式有很大的幫助。
1.5 分類(lèi)
設(shè)計(jì)模式有兩種分類(lèi)方法,即根據(jù)模式的目的來(lái)分和根據(jù)模式的作用的范圍來(lái)分。
1.5.1 根據(jù)目的來(lái)分
根據(jù)模式是用來(lái)完成什么工作來(lái)劃分,這種方式可分為創(chuàng)建型模式、結(jié)構(gòu)型模式和行為型模式 3 種。
創(chuàng)建型模式:用于描述“怎樣創(chuàng)建對(duì)象”,它的主要特點(diǎn)是“將對(duì)象的創(chuàng)建與使用分離”。GoF 中提供了單例、原型、工廠方法、抽象工廠、建造者等 5 種創(chuàng)建型模式。
結(jié)構(gòu)型模式:用于描述如何將類(lèi)或?qū)ο蟀茨撤N布局組成更大的結(jié)構(gòu),GoF 中提供了代理、適配器、橋接、裝飾、外觀、享元、組合等 7 種結(jié)構(gòu)型模式。
行為型模式:用于描述類(lèi)或?qū)ο笾g怎樣相互協(xié)作共同完成單個(gè)對(duì)象都無(wú)法單獨(dú)完成的任務(wù),以及怎樣分配職責(zé)。GoF 中提供了模板方法、策略、命令、職責(zé)鏈、狀態(tài)、觀察者、中介者、迭代器、訪問(wèn)者、備忘錄、解釋器等 11 種行為型模式。
1.5.2 根據(jù)作用范圍來(lái)分
根據(jù)模式是主要用于類(lèi)上還是主要用于對(duì)象上來(lái)分,這種方式可分為類(lèi)模式和對(duì)象模式兩種。
類(lèi)模式:用于處理類(lèi)與子類(lèi)之間的關(guān)系,這些關(guān)系通過(guò)繼承來(lái)建立,是靜態(tài)的,在編譯時(shí)刻便確定下來(lái)了。GoF中的工廠方法、(類(lèi))適配器、模板方法、解釋器屬于該模式。
對(duì)象模式:用于處理對(duì)象之間的關(guān)系,這些關(guān)系可以通過(guò)組合或聚合來(lái)實(shí)現(xiàn),在運(yùn)行時(shí)刻是可以變化的,更具動(dòng)態(tài)性。GoF 中除了以上 4 種,其他的都是對(duì)象模式。
表1 GoF 的 23 種設(shè)計(jì)模式的分類(lèi)表
| 范圍\目的 | 創(chuàng)建型模式 | 結(jié)構(gòu)型模式 | 行為型模式 |
|---|---|---|---|
| 類(lèi)模式 | 工廠方法 | (類(lèi))適配器 | 模板方法、解釋器 |
| 對(duì)象模式 | 單例 原型 抽象工廠 建造者 | 代理 (對(duì)象)適配器 橋接 裝飾 外觀 享元 組合 | 策略 命令 職責(zé)鏈 狀態(tài) 觀察者 中介者 迭代器 訪問(wèn)者 備忘錄 |
1.6 GoF的23種設(shè)計(jì)模式的功能
前面說(shuō)明了 GoF 的 23 種設(shè)計(jì)模式的分類(lèi),現(xiàn)在對(duì)各個(gè)模式的功能進(jìn)行介紹。 單例(Singleton)模式:某個(gè)類(lèi)只能生成一個(gè)實(shí)例,該類(lèi)提供了一個(gè)全局訪問(wèn)點(diǎn)供外部獲取該實(shí)例,其拓展是有限多例模式。 原型(Prototype)模式:將一個(gè)對(duì)象作為原型,通過(guò)對(duì)其進(jìn)行復(fù)制而克隆出多個(gè)和原型類(lèi)似的新實(shí)例。 工廠方法(Factory Method)模式:定義一個(gè)用于創(chuàng)建產(chǎn)品的接口,由子類(lèi)決定生產(chǎn)什么產(chǎn)品。 抽象工廠(AbstractFactory)模式:提供一個(gè)創(chuàng)建產(chǎn)品族的接口,其每個(gè)子類(lèi)可以生產(chǎn)一系列相關(guān)的產(chǎn)品。 建造者(Builder)模式:將一個(gè)復(fù)雜對(duì)象分解成多個(gè)相對(duì)簡(jiǎn)單的部分,然后根據(jù)不同需要分別創(chuàng)建它們,最后構(gòu)建成該復(fù)雜對(duì)象。 代理(Proxy)模式:為某對(duì)象提供一種代理以控制對(duì)該對(duì)象的訪問(wèn)。即客戶端通過(guò)代理間接地訪問(wèn)該對(duì)象,從而限制、增強(qiáng)或修改該對(duì)象的一些特性。 適配器(Adapter)模式:將一個(gè)類(lèi)的接口轉(zhuǎn)換成客戶希望的另外一個(gè)接口,使得原本由于接口不兼容而不能一起工作的那些類(lèi)能一起工作。 橋接(Bridge)模式:將抽象與實(shí)現(xiàn)分離,使它們可以獨(dú)立變化。它是用組合關(guān)系代替繼承關(guān)系來(lái)實(shí)現(xiàn),從而降低了抽象和實(shí)現(xiàn)這兩個(gè)可變維度的耦合度。 裝飾(Decorator)模式:動(dòng)態(tài)的給對(duì)象增加一些職責(zé),即增加其額外的功能。 外觀(Facade)模式:為多個(gè)復(fù)雜的子系統(tǒng)提供一個(gè)一致的接口,使這些子系統(tǒng)更加容易被訪問(wèn)。 享元(Flyweight)模式:運(yùn)用共享技術(shù)來(lái)有效地支持大量細(xì)粒度對(duì)象的復(fù)用。 組合(Composite)模式:將對(duì)象組合成樹(shù)狀層次結(jié)構(gòu),使用戶對(duì)單個(gè)對(duì)象和組合對(duì)象具有一致的訪問(wèn)性。 模板方法(TemplateMethod)模式:定義一個(gè)操作中的算法骨架,而將算法的一些步驟延遲到子類(lèi)中,使得子類(lèi)可以不改變?cè)撍惴ńY(jié)構(gòu)的情況下重定義該算法的某些特定步驟。 策略(Strategy)模式:定義了一系列算法,并將每個(gè)算法封裝起來(lái),使它們可以相互替換,且算法的改變不會(huì)影響使用算法的客戶。 命令(Command)模式:將一個(gè)請(qǐng)求封裝為一個(gè)對(duì)象,使發(fā)出請(qǐng)求的責(zé)任和執(zhí)行請(qǐng)求的責(zé)任分割開(kāi)。 職責(zé)鏈(Chain of Responsibility)模式:把請(qǐng)求從鏈中的一個(gè)對(duì)象傳到下一個(gè)對(duì)象,直到請(qǐng)求被響應(yīng)為止。通過(guò)這種方式去除對(duì)象之間的耦合。 狀態(tài)(State)模式:允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)發(fā)生改變時(shí)改變其行為能力。 觀察者(Observer)模式:多個(gè)對(duì)象間存在一對(duì)多關(guān)系,當(dāng)一個(gè)對(duì)象發(fā)生改變時(shí),把這種改變通知給其他多個(gè)對(duì)象,從而影響其他對(duì)象的行為。 中介者(Mediator)模式:定義一個(gè)中介對(duì)象來(lái)簡(jiǎn)化原有對(duì)象之間的交互關(guān)系,降低系統(tǒng)中對(duì)象間的耦合度,使原有對(duì)象之間不必相互了解。 迭代器(Iterator)模式:提供一種方法來(lái)順序訪問(wèn)聚合對(duì)象中的一系列數(shù)據(jù),而不暴露聚合對(duì)象的內(nèi)部表示。 訪問(wèn)者(Visitor)模式:在不改變集合元素的前提下,為一個(gè)集合中的每個(gè)元素提供多種訪問(wèn)方式,即每個(gè)元素有多個(gè)訪問(wèn)者對(duì)象訪問(wèn)。 備忘錄(Memento)模式:在不破壞封裝性的前提下,獲取并保存一個(gè)對(duì)象的內(nèi)部狀態(tài),以便以后恢復(fù)它。 解釋器(Interpreter)模式:提供如何定義語(yǔ)言的文法,以及對(duì)語(yǔ)言句子的解釋方法,即解釋器。
1.7 如何正確使用設(shè)計(jì)模式
設(shè)計(jì)模式要活學(xué)活用,不要生搬硬套。想要游刃有余地使用設(shè)計(jì)模式,需要打下牢固的程序設(shè)計(jì)語(yǔ)言基礎(chǔ)、夯實(shí)自己的編程思想、積累大量的時(shí)間經(jīng)驗(yàn)、提高開(kāi)發(fā)能力。目的都是讓程序低耦合,高復(fù)用,高內(nèi)聚,易擴(kuò)展,易維護(hù)。使用設(shè)計(jì)模式可以從以下幾點(diǎn)出發(fā)或?qū)W習(xí):
1.7.1. 需求驅(qū)動(dòng)
不僅僅是功能性需求,需求驅(qū)動(dòng)還包括性能和運(yùn)行時(shí)的需求,如軟件的可維護(hù)性和可復(fù)用性等方面。設(shè)計(jì)模式是針對(duì)軟件設(shè)計(jì)的,而軟件設(shè)計(jì)是針對(duì)需求的,一定不要為了使用設(shè)計(jì)模式而使用設(shè)計(jì)模式,否則可能會(huì)使設(shè)計(jì)變得復(fù)雜,使軟件難以調(diào)試和維護(hù)。
1.7.2. 分析成功的模式應(yīng)用項(xiàng)目
對(duì)現(xiàn)有的應(yīng)用實(shí)例進(jìn)行分析是一個(gè)很好的學(xué)習(xí)途徑,應(yīng)當(dāng)注意學(xué)習(xí)已有的項(xiàng)目,而不僅是學(xué)習(xí)設(shè)計(jì)模式如何實(shí)現(xiàn),更重要的是注意在什么場(chǎng)合使用設(shè)計(jì)模式。
1.7.3. 充分了解所使用的開(kāi)發(fā)平臺(tái)
設(shè)計(jì)模式大部分都是針對(duì)面向?qū)ο蟮能浖O(shè)計(jì),因此在理論上適合任何面向?qū)ο蟮恼Z(yǔ)言,但隨著技術(shù)的發(fā)展和編程環(huán)境的改善,設(shè)計(jì)模式的實(shí)現(xiàn)方式會(huì)有很大的差別。在一些平臺(tái)下,某些設(shè)計(jì)模式是自然實(shí)現(xiàn)的。
不僅指編程語(yǔ)言,平臺(tái)還包括平臺(tái)引入的技術(shù)。例如,Java EE 引入了反射機(jī)制和依賴注入,這些技術(shù)的使用使設(shè)計(jì)模式的實(shí)現(xiàn)方式產(chǎn)生了改變。
1.7.4. 在編程中領(lǐng)悟模式
軟件開(kāi)發(fā)是一項(xiàng)實(shí)踐工作,最直接的方法就是編程。沒(méi)有從來(lái)不下棋卻熟悉定式的圍棋高手,也沒(méi)有不會(huì)編程就能成為架構(gòu)設(shè)計(jì)師的先例。掌握設(shè)計(jì)模式是水到渠成的事情,除了理論只是和實(shí)踐積累,可能會(huì)“漸悟”或者“頓悟”。
1.7.5.避免設(shè)計(jì)過(guò)度
設(shè)計(jì)模式解決的是設(shè)計(jì)不足的問(wèn)題,但同時(shí)也要避免設(shè)計(jì)過(guò)度。一定要牢記簡(jiǎn)潔原則,要知道設(shè)計(jì)模式是為了使設(shè)計(jì)簡(jiǎn)單,而不是更復(fù)雜。如果引入設(shè)計(jì)模式使得設(shè)計(jì)變得復(fù)雜,只能說(shuō)我們把簡(jiǎn)單問(wèn)題復(fù)雜化了,問(wèn)題本身不需要設(shè)計(jì)模式。
這里需要把握的是需求變化的程度,一定要區(qū)分需求的穩(wěn)定部分和可變部分。一個(gè)軟件必然有穩(wěn)定部分,這個(gè)部分就是核心業(yè)務(wù)邏輯。如果核心業(yè)務(wù)邏輯發(fā)生變化,軟件就沒(méi)有存在的必要,核心業(yè)務(wù)邏輯是我們需要固化的。對(duì)于可變的部分,需要判斷可能發(fā)生變化的程度來(lái)確定設(shè)計(jì)策略和設(shè)計(jì)風(fēng)險(xiǎn)。要知道,設(shè)計(jì)過(guò)度與設(shè)計(jì)不足同樣對(duì)項(xiàng)目有害。
1.8 設(shè)計(jì)原則的好處
在軟件開(kāi)發(fā)中,為了提高軟件系統(tǒng)的可維護(hù)性和可復(fù)用性,增加軟件的可擴(kuò)展性和靈活性,程序員要盡量根據(jù) 7 條原則來(lái)開(kāi)發(fā)程序,從而提高軟件開(kāi)發(fā)效率、節(jié)約軟件開(kāi)發(fā)成本和維護(hù)成本。
二、開(kāi)閉原則
2.1 定義
開(kāi)閉原則(Open Closed Principle,OCP)由勃蘭特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向?qū)ο筌浖?gòu)造》(Object Oriented Software Construction)中提出:軟件實(shí)體應(yīng)當(dāng)對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉(Software entities should be open for extension,but closed for modification),這就是開(kāi)閉原則的經(jīng)典定義。
這里的軟件實(shí)體包括以下幾個(gè)部分:
項(xiàng)目中劃分出的模塊
類(lèi)與接口
方法
開(kāi)閉原則的含義是:當(dāng)應(yīng)用的需求改變時(shí),在不修改軟件實(shí)體的源代碼或者二進(jìn)制代碼的前提下,可以擴(kuò)展模塊的功能,使其滿足新的需求。
2.2 作用
開(kāi)閉原則是面向?qū)ο蟪绦蛟O(shè)計(jì)的終極目標(biāo),它使軟件實(shí)體擁有一定的適應(yīng)性和靈活性的同時(shí)具備穩(wěn)定性和延續(xù)性。具體來(lái)說(shuō),其作用如下。
2.2 .1. 對(duì)軟件測(cè)試的影響
軟件遵守開(kāi)閉原則的話,軟件測(cè)試時(shí)只需要對(duì)擴(kuò)展的代碼進(jìn)行測(cè)試就可以了,因?yàn)樵械臏y(cè)試代碼仍然能夠正常運(yùn)行。
2.2 .2. 可以提高代碼的可復(fù)用性
粒度越小,被復(fù)用的可能性就越大;在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,根據(jù)原子和抽象編程可以提高代碼的可復(fù)用性。
2.2 .3. 可以提高軟件的可維護(hù)性
遵守開(kāi)閉原則的軟件,其穩(wěn)定性高和延續(xù)性強(qiáng),從而易于擴(kuò)展和維護(hù)。
2.3 開(kāi)閉原則的實(shí)現(xiàn)方法
可以通過(guò)“抽象約束、封裝變化”來(lái)實(shí)現(xiàn)開(kāi)閉原則,即通過(guò)接口或者抽象類(lèi)為軟件實(shí)體定義一個(gè)相對(duì)穩(wěn)定的抽象層,而將相同的可變因素封裝在相同的具體實(shí)現(xiàn)類(lèi)中。
因?yàn)槌橄箪`活性好,適應(yīng)性廣,只要抽象的合理,可以基本保持軟件架構(gòu)的穩(wěn)定。而軟件中易變的細(xì)節(jié)可以從抽象派生來(lái)的實(shí)現(xiàn)類(lèi)來(lái)進(jìn)行擴(kuò)展,當(dāng)軟件需要發(fā)生變化時(shí),只需要根據(jù)需求重新派生一個(gè)實(shí)現(xiàn)類(lèi)來(lái)擴(kuò)展就可以了。
三、里氏替換原則
3.1 定義
里氏替換原則主要闡述了有關(guān)繼承的一些原則,也就是什么時(shí)候應(yīng)該使用繼承,什么時(shí)候不應(yīng)該使用繼承,以及其中蘊(yùn)含的原理。里氏替換原是繼承復(fù)用的基礎(chǔ),它反映了基類(lèi)與子類(lèi)之間的關(guān)系,是對(duì)開(kāi)閉原則的補(bǔ)充,是對(duì)實(shí)現(xiàn)抽象化的具體步驟的規(guī)范。
3.2 作用
里氏替換原則的主要作用如下。
3.2.1 里氏替換原則是實(shí)現(xiàn)開(kāi)閉原則的重要方式之一;
3.2.2 它克服了繼承中重寫(xiě)父類(lèi)造成的可復(fù)用性變差的缺點(diǎn);
3.2.3 它是動(dòng)作正確性的保證。即類(lèi)的擴(kuò)展不會(huì)給已有的系統(tǒng)引入新的錯(cuò)誤,降低了代碼出錯(cuò)的可能性;
3.2.4 加強(qiáng)程序的健壯性,同時(shí)變更時(shí)可以做到非常好的兼容性,提高程序的維護(hù)性、可擴(kuò)展性,降低需求變更時(shí)引入的風(fēng)險(xiǎn)。
3.3 實(shí)現(xiàn)方法
里氏替換原則通俗來(lái)講就是:子類(lèi)可以擴(kuò)展父類(lèi)的功能,但不能改變父類(lèi)原有的功能。也就是說(shuō):子類(lèi)繼承父類(lèi)時(shí),除添加新的方法完成新增功能外,盡量不要重寫(xiě)父類(lèi)的方法。
根據(jù)上述理解,對(duì)里氏替換原則的定義可以總結(jié)如下:
子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法,但不能覆蓋父類(lèi)的非抽象方法
子類(lèi)中可以增加自己特有的方法
當(dāng)子類(lèi)的方法重載父類(lèi)的方法時(shí),方法的前置條件(即方法的輸入?yún)?shù))要比父類(lèi)的方法更寬松
當(dāng)子類(lèi)的方法實(shí)現(xiàn)父類(lèi)的方法時(shí)(重寫(xiě)/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(即方法的的輸出/返回值)要比父類(lèi)的方法更嚴(yán)格或相等
通過(guò)重寫(xiě)父類(lèi)的方法來(lái)完成新的功能寫(xiě)起來(lái)雖然簡(jiǎn)單,但是整個(gè)繼承體系的可復(fù)用性會(huì)比較差,特別是運(yùn)用多態(tài)比較頻繁時(shí),程序運(yùn)行出錯(cuò)的概率會(huì)非常大。
如果程序違背了里氏替換原則,則繼承類(lèi)的對(duì)象在基類(lèi)出現(xiàn)的地方會(huì)出現(xiàn)運(yùn)行錯(cuò)誤。這時(shí)其修正方法是:取消原來(lái)的繼承關(guān)系,重新設(shè)計(jì)它們之間的關(guān)系。
四、依賴倒置原則
4.1 定義
依賴倒置原則(Dependence Inversion Principle,DIP)是 Object Mentor 公司總裁羅伯特·馬?。≧obert C.Martin)于 1996 年在 C++ Report 上發(fā)表的文章。
依賴倒置原則的原始定義為:高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象(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)。其核心思想是:要面向接口編程,不要面向?qū)崿F(xiàn)編程。
依賴倒置原則是實(shí)現(xiàn)開(kāi)閉原則的重要途徑之一,它降低了客戶與實(shí)現(xiàn)模塊之間的耦合。
由于在軟件設(shè)計(jì)中,細(xì)節(jié)具有多變性,而抽象層則相對(duì)穩(wěn)定,因此以抽象為基礎(chǔ)搭建起來(lái)的架構(gòu)要比以細(xì)節(jié)為基礎(chǔ)搭建起來(lái)的架構(gòu)要穩(wěn)定得多。這里的抽象指的是接口或者抽象類(lèi),而細(xì)節(jié)是指具體的實(shí)現(xiàn)類(lèi)。
使用接口或者抽象類(lèi)的目的是制定好規(guī)范和契約,而不去涉及任何具體的操作,把展現(xiàn)細(xì)節(jié)的任務(wù)交給它們的實(shí)現(xiàn)類(lèi)去完成。
4.2 作用
依賴倒置原則可以降低類(lèi)間的耦合性。
依賴倒置原則可以提高系統(tǒng)的穩(wěn)定性。
依賴倒置原則可以減少并行開(kāi)發(fā)引起的風(fēng)險(xiǎn)。
依賴倒置原則可以提高代碼的可讀性和可維護(hù)性。
4.3 實(shí)現(xiàn)方法
依賴倒置原則的目的是通過(guò)要面向接口的編程來(lái)降低類(lèi)間的耦合性,所以我們?cè)趯?shí)際編程中只要遵循以下4點(diǎn),就能在項(xiàng)目中滿足這個(gè)規(guī)則。
每個(gè)類(lèi)盡量提供接口或抽象類(lèi),或者兩者都具備。
變量的聲明類(lèi)型盡量是接口或者是抽象類(lèi)。
任何類(lèi)都不應(yīng)該從具體類(lèi)派生。
使用繼承時(shí)盡量遵循里氏替換原則。
五、單一職責(zé)原則
5.1 定義
單一職責(zé)原則(Single Responsibility Principle,SRP)又稱單一功能原則,由羅伯特·C.馬?。≧obert C. Martin)于《敏捷軟件開(kāi)發(fā):原則、模式和實(shí)踐》一書(shū)中提出的。這里的職責(zé)是指類(lèi)變化的原因,單一職責(zé)原則規(guī)定一個(gè)類(lèi)應(yīng)該有且僅有一個(gè)引起它變化的原因,否則類(lèi)應(yīng)該被拆分(There should never be more than one reason for a class to change)。
該原則提出對(duì)象不應(yīng)該承擔(dān)太多職責(zé),如果一個(gè)對(duì)象承擔(dān)了太多的職責(zé),至少存在以下兩個(gè)缺點(diǎn):
一個(gè)職責(zé)的變化可能會(huì)削弱或者抑制這個(gè)類(lèi)實(shí)現(xiàn)其他職責(zé)的能力;
當(dāng)客戶端需要該對(duì)象的某一個(gè)職責(zé)時(shí),不得不將其他不需要的職責(zé)全都包含進(jìn)來(lái),從而造成冗余代碼或代碼的浪費(fèi)。
5.2 單一職責(zé)原則的優(yōu)點(diǎn)
單一職責(zé)原則的核心就是控制類(lèi)的粒度大小、將對(duì)象解耦、提高其內(nèi)聚性。如果遵循單一職責(zé)原則將有以下優(yōu)點(diǎn)。
降低類(lèi)的復(fù)雜度。一個(gè)類(lèi)只負(fù)責(zé)一項(xiàng)職責(zé),其邏輯肯定要比負(fù)責(zé)多項(xiàng)職責(zé)簡(jiǎn)單得多。
提高類(lèi)的可讀性。復(fù)雜性降低,自然其可讀性會(huì)提高。
提高系統(tǒng)的可維護(hù)性??勺x性提高,那自然更容易維護(hù)了。
變更引起的風(fēng)險(xiǎn)降低。變更是必然的,如果單一職責(zé)原則遵守得好,當(dāng)修改一個(gè)功能時(shí),可以顯著降低對(duì)其他功能的影響。
5.3 單一職責(zé)原則的實(shí)現(xiàn)方法
單一職責(zé)原則是最簡(jiǎn)單但又最難運(yùn)用的原則,需要設(shè)計(jì)人員發(fā)現(xiàn)類(lèi)的不同職責(zé)并將其分離,再封裝到不同的類(lèi)或模塊中。而發(fā)現(xiàn)類(lèi)的多重職責(zé)需要設(shè)計(jì)人員具有較強(qiáng)的分析設(shè)計(jì)能力和相關(guān)重構(gòu)經(jīng)驗(yàn)
六、接口隔離原則
6.1 定義
接口隔離原則(Interface Segregation Principle,ISP)要求程序員盡量將臃腫龐大的接口拆分成更小的和更具體的接口,讓接口中只包含客戶感興趣的方法。
2002 年羅伯特·C.馬丁給“接口隔離原則”的定義是:客戶端不應(yīng)該被迫依賴于它不使用的方法(Clients should not be forced to depend on methods they do not use)。該原則還有另外一個(gè)定義:一個(gè)類(lèi)對(duì)另一個(gè)類(lèi)的依賴應(yīng)該建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。
以上兩個(gè)定義的含義是:要為各個(gè)類(lèi)建立它們需要的專(zhuān)用接口,而不要試圖去建立一個(gè)很龐大的接口供所有依賴它的類(lèi)去調(diào)用。
接口隔離原則和單一職責(zé)都是為了提高類(lèi)的內(nèi)聚性、降低它們之間的耦合性,體現(xiàn)了封裝的思想,但兩者是不同的:
單一職責(zé)原則注重的是職責(zé),而接口隔離原則注重的是對(duì)接口依賴的隔離。
單一職責(zé)原則主要是約束類(lèi),它針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié);接口隔離原則主要約束接口,主要針對(duì)抽象和程序整體框架的構(gòu)建。
6.2 優(yōu)點(diǎn)
接口隔離原則是為了約束接口、降低類(lèi)對(duì)接口的依賴性,遵循接口隔離原則有以下 5 個(gè)優(yōu)點(diǎn)。
將臃腫龐大的接口分解為多個(gè)粒度小的接口,可以預(yù)防外來(lái)變更的擴(kuò)散,提高系統(tǒng)的靈活性和可維護(hù)性。
接口隔離提高了系統(tǒng)的內(nèi)聚性,減少了對(duì)外交互,降低了系統(tǒng)的耦合性。
如果接口的粒度大小定義合理,能夠保證系統(tǒng)的穩(wěn)定性;但是,如果定義過(guò)小,則會(huì)造成接口數(shù)量過(guò)多,使設(shè)計(jì)復(fù)雜化;如果定義太大,靈活性降低,無(wú)法提供定制服務(wù),給整體項(xiàng)目帶來(lái)無(wú)法預(yù)料的風(fēng)險(xiǎn)。
使用多個(gè)專(zhuān)門(mén)的接口還能夠體現(xiàn)對(duì)象的層次,因?yàn)榭梢酝ㄟ^(guò)接口的繼承,實(shí)現(xiàn)對(duì)總接口的定義。
能減少項(xiàng)目工程中的代碼冗余。過(guò)大的大接口里面通常放置許多不用的方法,當(dāng)實(shí)現(xiàn)這個(gè)接口的時(shí)候,被迫設(shè)計(jì)冗余的代碼。
6.3 接口隔離原則的實(shí)現(xiàn)方法
接口盡量小,但是要有限度。一個(gè)接口只服務(wù)于一個(gè)子模塊或業(yè)務(wù)邏輯。
為依賴接口的類(lèi)定制服務(wù)。只提供調(diào)用者需要的方法,屏蔽不需要的方法。
了解環(huán)境,拒絕盲從。每個(gè)項(xiàng)目或產(chǎn)品都有選定的環(huán)境因素,環(huán)境不同,接口拆分的標(biāo)準(zhǔn)就不同深入了解業(yè)務(wù)邏輯。
提高內(nèi)聚,減少對(duì)外交互。使接口用最少的方法去完成最多的事情。
七、迪米特法則
7.1 定義
迪米特法則(Law of Demeter,LoD)又叫作最少知識(shí)原則(Least Knowledge Principle,LKP),產(chǎn)生于 1987 年美國(guó)東北大學(xué)(Northeastern University)的一個(gè)名為迪米特(Demeter)的研究項(xiàng)目,由伊恩·荷蘭(Ian Holland)提出,被 UML 創(chuàng)始者之一的布奇(Booch)普及,后來(lái)又因?yàn)樵诮?jīng)典著作《程序員修煉之道》(The Pragmatic Programmer)提及而廣為人知。
迪米特法則的定義是:只與你的直接朋友交談,不跟“陌生人”說(shuō)話(Talk only to your immediate friends and not to strangers)。其含義是:如果兩個(gè)軟件實(shí)體無(wú)須直接通信,那么就不應(yīng)當(dāng)發(fā)生直接的相互調(diào)用,可以通過(guò)第三方轉(zhuǎn)發(fā)該調(diào)用。其目的是降低類(lèi)之間的耦合度,提高模塊的相對(duì)獨(dú)立性。
迪米特法則中的“朋友”是指:當(dāng)前對(duì)象本身、當(dāng)前對(duì)象的成員對(duì)象、當(dāng)前對(duì)象所創(chuàng)建的對(duì)象、當(dāng)前對(duì)象的方法參數(shù)等,這些對(duì)象同當(dāng)前對(duì)象存在關(guān)聯(lián)、聚合或組合關(guān)系,可以直接訪問(wèn)這些對(duì)象的方法。
7.2 優(yōu)點(diǎn)
降低了類(lèi)之間的耦合度,提高了模塊的相對(duì)獨(dú)立性。
由于親合度降低,從而提高了類(lèi)的可復(fù)用率和系統(tǒng)的擴(kuò)展性。
7.3 實(shí)現(xiàn)方法
從迪米特法則的定義和特點(diǎn)可知,它強(qiáng)調(diào)以下兩點(diǎn):
從依賴者的角度來(lái)說(shuō),只依賴應(yīng)該依賴的對(duì)象。
從被依賴者的角度說(shuō),只暴露應(yīng)該暴露的方法。
所以,在運(yùn)用迪米特法則時(shí)要注意以下 6 點(diǎn)。
在類(lèi)的劃分上,應(yīng)該創(chuàng)建弱耦合的類(lèi)。類(lèi)與類(lèi)之間的耦合越弱,就越有利于實(shí)現(xiàn)可復(fù)用的目標(biāo)。
在類(lèi)的結(jié)構(gòu)設(shè)計(jì)上,盡量降低類(lèi)成員的訪問(wèn)權(quán)限。
在類(lèi)的設(shè)計(jì)上,優(yōu)先考慮將一個(gè)類(lèi)設(shè)置成不變類(lèi)。
在對(duì)其他類(lèi)的引用上,將引用其他對(duì)象的次數(shù)降到最低。
不暴露類(lèi)的屬性成員,而應(yīng)該提供相應(yīng)的訪問(wèn)器(set 和 get 方法)。
謹(jǐn)慎使用序列化(Serializable)功能。
八、合成復(fù)用原則
8.1 定義
合成復(fù)用原則(Composite Reuse Principle,CRP)又叫組合/聚合復(fù)用原則(Composition/Aggregate Reuse Principle,CARP)。它要求在軟件復(fù)用時(shí),要盡量先使用組合或者聚合等關(guān)聯(lián)關(guān)系來(lái)實(shí)現(xiàn),其次才考慮使用繼承關(guān)系來(lái)實(shí)現(xiàn)。
如果要使用繼承關(guān)系,則必須嚴(yán)格遵循里氏替換原則。合成復(fù)用原則同里氏替換原則相輔相成的,兩者都是開(kāi)閉原則的具體實(shí)現(xiàn)規(guī)范。
8.2 重要性
通常類(lèi)的復(fù)用分為繼承復(fù)用和合成復(fù)用兩種,繼承復(fù)用雖然有簡(jiǎn)單和易實(shí)現(xiàn)的優(yōu)點(diǎn),但它也存在以下缺點(diǎn)。
繼承復(fù)用破壞了類(lèi)的封裝性。因?yàn)槔^承會(huì)將父類(lèi)的實(shí)現(xiàn)細(xì)節(jié)暴露給子類(lèi),父類(lèi)對(duì)子類(lèi)是透明的,所以這種復(fù)用又稱為“白箱”復(fù)用。
子類(lèi)與父類(lèi)的耦合度高。父類(lèi)的實(shí)現(xiàn)的任何改變都會(huì)導(dǎo)致子類(lèi)的實(shí)現(xiàn)發(fā)生變化,這不利于類(lèi)的擴(kuò)展與維護(hù)。
它限制了復(fù)用的靈活性。從父類(lèi)繼承而來(lái)的實(shí)現(xiàn)是靜態(tài)的,在編譯時(shí)已經(jīng)定義,所以在運(yùn)行時(shí)不可能發(fā)生變化。
采用組合或聚合復(fù)用時(shí),可以將已有對(duì)象納入新對(duì)象中,使之成為新對(duì)象的一部分,新對(duì)象可以調(diào)用已有對(duì)象的功能,它有以下優(yōu)點(diǎn)。
它維持了類(lèi)的封裝性。因?yàn)槌煞謱?duì)象的內(nèi)部細(xì)節(jié)是新對(duì)象看不見(jiàn)的,所以這種復(fù)用又稱為“黑箱”復(fù)用。
新舊類(lèi)之間的耦合度低。這種復(fù)用所需的依賴較少,新對(duì)象存取成分對(duì)象的唯一方法是通過(guò)成分對(duì)象的接口。
復(fù)用的靈活性高。這種復(fù)用可以在運(yùn)行時(shí)動(dòng)態(tài)進(jìn)行,新對(duì)象可以動(dòng)態(tài)地引用與成分對(duì)象類(lèi)型相同的對(duì)象。
8.3 實(shí)現(xiàn)方法
合成復(fù)用原則是通過(guò)將已有的對(duì)象納入新對(duì)象中,作為新對(duì)象的成員對(duì)象來(lái)實(shí)現(xiàn)的,新對(duì)象可以調(diào)用已有對(duì)象的功能,從而達(dá)到復(fù)用。
九、總結(jié)
這 7 種設(shè)計(jì)原則是軟件設(shè)計(jì)模式必須盡量遵循的原則,是設(shè)計(jì)模式的基礎(chǔ)。在實(shí)際開(kāi)發(fā)過(guò)程中,并不是一定要求所有代碼都遵循設(shè)計(jì)原則,而是要綜合考慮人力、時(shí)間、成本、質(zhì)量,不刻意追求完美,要在適當(dāng)?shù)膱?chǎng)景遵循設(shè)計(jì)原則。這體現(xiàn)的是一種平衡取舍,可以幫助我們?cè)O(shè)計(jì)出更加優(yōu)雅的代碼結(jié)構(gòu)。
| 設(shè)計(jì)原則 | 一句話歸納 | 目的 |
|---|---|---|
| 開(kāi)閉原則 | 對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉 | 降低維護(hù)帶來(lái)的新風(fēng)險(xiǎn) |
| 依賴倒置原則 | 高層不應(yīng)該依賴低層,要面向接口編程 | 更利于代碼結(jié)構(gòu)的升級(jí)擴(kuò)展 |
| 單一職責(zé)原則 | 一個(gè)類(lèi)只干一件事,實(shí)現(xiàn)類(lèi)要單一 | 便于理解,提高代碼的可讀性 |
| 接口隔離原則 | 一個(gè)接口只干一件事,接口要精簡(jiǎn)單一 | 功能解耦,高聚合、低耦合 |
| 迪米特法則 | 不該知道的不要知道,一個(gè)類(lèi)應(yīng)該保持對(duì)其它對(duì)象最少的了解,降低耦合度 | 只和朋友交流,不和陌生人說(shuō)話,減少代碼臃腫 |
| 里氏替換原則 | 不要破壞繼承體系,子類(lèi)重寫(xiě)方法功能發(fā)生改變,不應(yīng)該影響父類(lèi)方法的含義 | 防止繼承泛濫 |
| 合成復(fù)用原則 | 盡量使用組合或者聚合關(guān)系實(shí)現(xiàn)代碼復(fù)用,少使用繼承 | 降低代碼耦合 |
實(shí)際上,這些原則的目的只有一個(gè):降低對(duì)象之間的耦合,增加程序的可復(fù)用性、可擴(kuò)展性和可維護(hù)性。