“是一個(gè)…的非凡類型”,而非“是一個(gè)由…所扮演的角色” 通過。乘 客和代理人都是非凡類型的人所扮演的角色。 n 永遠(yuǎn)不需要轉(zhuǎn)化 通過。一個(gè)Passenger對象將保持不變;Agent對象亦然。 n 擴(kuò)展,而非重寫和廢除 F 通過。 n 不要擴(kuò)展一個(gè)工具類 F 通過。 n 在問題域內(nèi),特指一種角色,交易或設(shè)備 F 通過。PersonRole是一種類型的角色。 繼續(xù)適用于此處! 繼續(xù)/組合示例3
n “是一個(gè)…的非凡類型”,而非“是一個(gè)由…所扮演的角色” F 通過。預(yù)訂和購買都是一種非凡類型的交易。 n 永遠(yuǎn)不需要轉(zhuǎn)化 F 通過。一個(gè)Reservation對象將保持不變;Purchase對象亦然。 n 擴(kuò)展,而非重寫和廢除 F 通過。 n 不要擴(kuò)展一個(gè)工具類 F 通過。 n 在問題域內(nèi),特指一種角色,交易或設(shè)備 F 通過。是一種交易。 繼續(xù)適用于此處! 繼續(xù)/組合示例4
n “是一個(gè)…的非凡類型”,而非“是一個(gè)由…所扮演的角色” F 失敗。預(yù)訂不是一種非凡類型的observable。 n 永遠(yuǎn)不需要轉(zhuǎn)化 F 通過。一個(gè)Reservation對象將保持不變。 n 擴(kuò)展,而非重寫和廢除 F 通過。 n 不要擴(kuò)展一個(gè)工具類 F 失敗。Observable就是一個(gè)工具類。 n 在問題域內(nèi),特指一種角色,交易或設(shè)備 F 不適用。Observable是一個(gè)工具類,并非一個(gè)問題域的類。。 繼續(xù)并非適用于此處! 繼續(xù)/組合總結(jié) n 組合與繼續(xù)都是重要的重用方法 n 在OO開發(fā)的早期,繼續(xù)被過度地使用 n 隨著時(shí)間的發(fā)展,我們發(fā)現(xiàn)優(yōu)先使用組合可以獲得重用性與簡單性更佳的設(shè)計(jì) n 當(dāng)然可以通過繼續(xù),以擴(kuò)充(enlarge)可用的組合類集(the set of composable classes)。 n 因此組合與繼續(xù)可以一起工作 n 但是我們的基本法則是: 優(yōu)先使用對象組合,而非(類)繼續(xù) [ Favor Composition Over Inheritance ] 法則2:針對接口編程,而非(接口的)實(shí)現(xiàn) [ Program To An Interface, Not An Implementation ] 接口 n 接口是一個(gè)對象在對其它的對象進(jìn)行調(diào)用時(shí)所知道的方法集合。 n 一個(gè)對象可以有多個(gè)接口(實(shí)際上,接口是對象所有方法的一個(gè)子集) n 類型是對象的一個(gè)特定的接口。 n 不同的對象可以具有相同的類型,而且一個(gè)對象可以具有多個(gè)不同的類型。 n 一個(gè)對象僅能通過其接口才會(huì)被其它對象所了解。 n 某種意義上,接口是以一種非常局限的方式,將“是一種…”表達(dá)為“一種支持該接口的…”。 n 接口是實(shí)現(xiàn)插件化(pluggability)的要害 實(shí)現(xiàn)繼續(xù)和接口繼續(xù) n 實(shí)現(xiàn)繼續(xù)(類繼續(xù)):一個(gè)對象的實(shí)現(xiàn)是根據(jù)另一個(gè)對象的實(shí)現(xiàn)來定義的。 n 接口繼續(xù)(子類型化):描述了一個(gè)對象可在什么時(shí)候被用來替代另一個(gè)對象。 n C++的繼續(xù)機(jī)制既指類繼續(xù),又指接口繼續(xù)。 n C++通過繼續(xù)純虛類來實(shí)現(xiàn)接口繼續(xù)。 n Java對接口繼續(xù)具有單獨(dú)的語言構(gòu)造方式-Java接口。 n Java接口構(gòu)造方式更加易于表達(dá)和實(shí)現(xiàn)那些專注于對象接口的設(shè)計(jì)。 接口的好處 n 優(yōu)點(diǎn): F Client不必知道其使用對象的具體所屬類。 F 一個(gè)對象可以很輕易地被(實(shí)現(xiàn)了相同接口的)的另一個(gè)對象所替換。 F 對象間的連接不必硬綁定(hardwire)到一個(gè)具體類的對象上,因此增加了靈活性。 F 松散藕合(loosens coupling)。 F 增加了重用的可能性。 F 提高了(對象)組合的機(jī)率,因?yàn)楸话瑢ο罂梢允侨魏螌?shí)現(xiàn)了一個(gè)指定接口的類。 n 缺點(diǎn): F 設(shè)計(jì)的復(fù)雜性略有增加 (譯者注:接口表示“…像…”(LikeA)的關(guān)系,繼續(xù)表示“…是…”(IsA)的關(guān)系,組合表示“…有…”(HasA)的關(guān)系。) 接口實(shí)例
n 該方法是指其它的一些類可以進(jìn)行交通工具的駕駛,而不必關(guān)心其實(shí)際上是(汽車,輪船,潛艇或是其它任何實(shí)現(xiàn)了IManeuverabre的對象)。 法則3:開放-封閉法則(OCP) 軟件組成實(shí)體應(yīng)該是可擴(kuò)展的,但是不可修改的。 [ Software Entities Should Be Open For Extension, Yet Closed For Modification ] 開放-封閉法則 n 開放-封閉法則認(rèn)為我們應(yīng)該試圖去設(shè)計(jì)出永遠(yuǎn)也不需要改變的模塊。 n 我們可以添加新代碼來擴(kuò)展系統(tǒng)的行為。我們不能對已有的代碼進(jìn)行修改。 n 符合OCP的模塊需滿足兩個(gè)標(biāo)準(zhǔn): F 可擴(kuò)展,即“對擴(kuò)展是開放的”(Open For Extension)-模塊的行為可以被擴(kuò)展,以需要滿足新的需求。 F 不可更改,即“對更改是封閉的”(Closed for Modification)-模塊的源代碼是不答應(yīng)進(jìn)行改動(dòng)的。 n 我們能如何去做呢? F 抽象(Abstraction) F 多態(tài)(Polymorphism) F 繼續(xù)(Inheritance) F 接口(Interface)
n 一個(gè)軟件系統(tǒng)的所有模塊不可能都滿足OCP,但是我們應(yīng)該努力最小化這些不滿足OCP的模塊數(shù)量。 n 開放-封閉法則是OO設(shè)計(jì)的真正核心。 n 符合該法則便意味著最高等級的復(fù)用性(reusability)和可維護(hù)性(maintainability)。 OCP示例 n 考慮下面某類的方法:
n 以上函數(shù)的工作是在制訂的部件數(shù)組中計(jì)算各個(gè)部件價(jià)格的總和。 n 若Part是一個(gè)基類或接口且使用了多態(tài),則該類可很輕易地來適應(yīng)新類型的部件,而不必對其進(jìn)行修改。 n 其將符合OCP
n 但是在計(jì)算總價(jià)格時(shí),若財(cái)務(wù)部頒布主板和內(nèi)存應(yīng)使用額外費(fèi)用,則將如何去做。 n 下列的代碼是如何來做的呢?
n 這符合OCP嗎? n 當(dāng)每次財(cái)務(wù)部提出新的計(jì)價(jià)策略,我們都不得不要修改totalPrice()方法!這并非“對更改是封閉的”。顯然,策略的變更便意味著我們不得不要在一些地方修改代碼的,因此我們該如何去做呢? n 為了使用我們第一個(gè)版本的totalPrice(),我們可以將計(jì)價(jià)策略合并到Part的getPrice()方法中。
n 這里是Part和ConcretePart類的示例:
n 但是現(xiàn)在每當(dāng)計(jì)價(jià)策略發(fā)生改變,我們就必須修改Part的每個(gè)子類! n 一個(gè)更好的思路是采用一個(gè)PricePolicy類,通過對其進(jìn)行繼續(xù)以提供不同的計(jì)價(jià)策略:
n 看起來我們所做的就是將問題推遲到另一個(gè)類中。但是使用該解決方案,我們可通過改變Part對象,在運(yùn)行期間動(dòng)態(tài)地來設(shè)定計(jì)價(jià)的策略。 n 另一個(gè)解決方案是使每個(gè)ConcretePart從數(shù)據(jù)庫或?qū)傩晕募蝎@取其當(dāng)前的價(jià)格。 單選法則 單選法則(the Single Choice Principle)是OCP的一個(gè)推論。 單選法則: 無論在什么時(shí)候,一個(gè)軟件系統(tǒng)必須支持一組備選項(xiàng),理想情況下,在系統(tǒng)中只能有一個(gè)類能夠知道整個(gè)的備選項(xiàng)集合。 法則4:Liskov替換法則(LSP) 使用指向基類(超類)的引用的函數(shù),必須能夠在不知道具體派生類(子類)對象類型的情況下使用它們。 [ Function Thar Use Referennces To Base(Super) Classes Must Be Able To Use Objects Of Derived(Sub) Classes Without Knowing It ] Liskov替換法則 n 顯而易見,Liskov替換法則(LSP)是根據(jù)我所熟知的“多態(tài)”而得出的。 n 例如:
n 方法drawShape應(yīng)該可與Sharp超類的任何子類一起工作(或者,若Sharp為Java接口,則該方法可與任何實(shí)現(xiàn)了Sharp接口的類一起工作) n 但是當(dāng)我們在實(shí)現(xiàn)子類時(shí)必須要謹(jǐn)慎對待,以確保我們不會(huì)無意中違反了LSP。
n 若一個(gè)函數(shù)未能滿足LSP,那么可能是因?yàn)樗@式地引用了超類的一些或所有子類。這樣的函數(shù)也違反了OCP,因?yàn)楫?dāng)我們創(chuàng)建一個(gè)新的子類時(shí),會(huì)不得不進(jìn)行代碼的修改。 LSP示例 n 考慮下面Rectangle類:
n 現(xiàn)在,Square類會(huì)如何呢?顯然,一個(gè)正方形是一個(gè)四邊形,因此Square類應(yīng)該從Rectangle類派生而來,對否?讓我們看一看! n 觀察可得: F 正方形不需要將高和寬都作為屬性,但是總之它將繼續(xù)自Rectangle。因此,每一個(gè)Square對象會(huì)浪費(fèi)一點(diǎn)內(nèi)存,但這并不是一個(gè)主要問題。 F 繼續(xù)而來的setWidth()和setHeight()方法對于Square而言并非真正地適合,因?yàn)橐粋€(gè)正方形的高和寬是相同。因此我們將需要重寫setWidth()和setHeight()方法。不得不重寫這些簡單的方法有可能是一種不恰當(dāng)?shù)睦^續(xù)使用方式。
n Square類如下:
n 看起來都還不錯(cuò)。但是讓我們檢驗(yàn)一下!
n 測試程序輸出:
n 看上去似乎我們違反了LSP!
n 這里的問題出在哪里呢?編寫testLsp()方法的程序員做了一個(gè)合理的假設(shè),即改變Rectangle的寬而保持它的高不變。 n 在將一個(gè)Square對象傳遞給這樣一個(gè)方法時(shí)產(chǎn)生了問題,顯然是違反了LSP n Square和Rectangle類是相互一致和合法的。盡管程序員對基類作了合理的假設(shè),但其所編寫的方法仍然會(huì)導(dǎo)致設(shè)計(jì)模型的失敗。 n 不能孤立地去看待解決方案,必須根據(jù)設(shè)計(jì)用戶所做的合理假設(shè)來看待它們。
n 一個(gè)數(shù)學(xué)意義上的正方形可能是一個(gè)四邊形,但是一個(gè)Square對象不是一個(gè)Rectangle對象,因?yàn)橐粋€(gè)Square對象的行為與一個(gè)Rectangle對象的行為是不一致的! n 從行為上來說,一個(gè)Square不是一個(gè)Rectangle!一個(gè)Square對象與一個(gè)Rectangle對象之間不具有多態(tài)的特征。 總結(jié) n Liskov替換法則(LSP)清楚地表明了ISA關(guān)系全部都是與行為有關(guān)的。 n 為了保持LSP(并與開放-封閉法則一起),所有子類必須符合使用基類的client所期望的行為。 n 一個(gè)子類型不得具有比基類型(base type)更多的限制,可能這對于基類型來說是合法的,但是可能會(huì)因?yàn)檫`反子類型的其中一個(gè)額外限制,從而違反了LSP! n LSP保證一個(gè)子類總是能夠被用在其基類可以出現(xiàn)的地方!