使用設(shè)計(jì)模式改善程序結(jié)構(gòu)(二)
發(fā)表時(shí)間:2024-01-20 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]在本系列的第一篇文章中,描述了如何通過設(shè)計(jì)模式來指導(dǎo)我們的程序重構(gòu)過程,并且著重介紹了設(shè)計(jì)模式意圖、動(dòng)機(jī)的重要性。在本文中我們將繼續(xù)上篇文章進(jìn)行討論,這次主要著重于設(shè)計(jì)模式的適用性,對于設(shè)計(jì)模式適用性的掌握有助于從另一個(gè)不同的方面來判斷一個(gè)設(shè)計(jì)模式是否真正適用于我們的實(shí)際問題,從而做出明智的選擇。...
在本系列的第一篇文章中,描述了如何通過設(shè)計(jì)模式來指導(dǎo)我們的程序重構(gòu)過程,并且著重介紹了設(shè)計(jì)模式意圖、動(dòng)機(jī)的重要性。在本文中我們將繼續(xù)上篇文章進(jìn)行討論,這次主要著重于設(shè)計(jì)模式的適用性,對于設(shè)計(jì)模式適用性的掌握有助于從另一個(gè)不同的方面來判斷一個(gè)設(shè)計(jì)模式是否真正適用于我們的實(shí)際問題,從而做出明智的選擇。
1、回顧
在上一篇文章中,我們給出了一個(gè)使用設(shè)計(jì)模式來改善程序結(jié)構(gòu)的例子,著重介紹了設(shè)計(jì)模式的意圖、動(dòng)機(jī)在我們程序重構(gòu)過程中的指導(dǎo)作用。
現(xiàn)在,我們將關(guān)注設(shè)計(jì)模式的另一個(gè)重要方面:設(shè)計(jì)模式的適用性。解決同一個(gè)問題一般會(huì)有多種方案或者模式,但是這些模式所關(guān)注的是同一個(gè)問題的不同方面,解決不同的需求,有各自的優(yōu)點(diǎn)和限制,各有各的解決之道。這就要求我們在選擇設(shè)計(jì)模式時(shí),對我們自己的問題有很好的理解:我們的需求是什么,我們要克服什么樣的限制,我們要獲得什么樣的特性等等。然后,可以看看我們想使用的解決問題的設(shè)計(jì)模式是否適用于我們的問題,如果不適用,是否可以使用其他的模式來彌補(bǔ),是否可以對這個(gè)設(shè)計(jì)模式進(jìn)行改進(jìn)使它符合我們的要求。
本文下面的部分,我們將對上一篇文章中的最終解決方案進(jìn)行進(jìn)一步的分析,來看看它到底滿足了我們什么樣的需求,又暴露了什么樣的不足,最后我們會(huì)給出一個(gè)更為符合要求的解決方案。
2、問題描述
在上一篇文章中,我們對一個(gè)網(wǎng)管系統(tǒng)中的錯(cuò)誤處理部分的代碼進(jìn)行了重構(gòu),最終使用了一個(gè)visitor設(shè)計(jì)模式解決了我們的問題。細(xì)心的讀者肯定會(huì)發(fā)現(xiàn),這個(gè)最終方案一樣存在著一個(gè)問題:如果錯(cuò)誤的類型不是固定不變的,而是隨著系統(tǒng)的進(jìn)展不斷增加的,會(huì)有什么結(jié)果呢?讓我們首先來看看上一篇文章中最終的類圖:
在這個(gè)類圖中,我增加了幾條依賴線,即ErrorHandler是依賴于DbError和CommError的。此時(shí)我們可以看到:ErrorBase依賴于ErrorHandler,ErrorHandler依賴于ErrorBase的派生類(DbError和CommError),而ErrorBase的派生類又要依賴于ErrorBase本身。這就形成了一個(gè)循環(huán)的依賴過程,這樣的結(jié)果就是ErrorBase傳遞地依賴于它的所有派生類。
這種循環(huán)依賴關(guān)系會(huì)帶來嚴(yán)重的問題,一旦ErrorBase新增了一個(gè)派生類,那么ErrorHandler類必須要被修改,由于ErrorBase又依賴于ErrorHandler,那么依賴于ErrorBase的所有類都需要重新編譯。這就意味著ErrorBase的所有派生類以及所有這些派生類的使用者都要重新編譯,這種大規(guī)模的重新編譯在開發(fā)一個(gè)分布式系統(tǒng)時(shí)會(huì)導(dǎo)致非常大的工作量,因?yàn)橐匦路植济恳粋(gè)重新編譯過的類,如果在重新分布時(shí)出現(xiàn)一些差錯(cuò)(如:忘記替換一些類),就會(huì)導(dǎo)致微妙的錯(cuò)誤,而且很不容易查找出來。
另外,在該模式中存在一個(gè)假設(shè),就是默認(rèn)任意一個(gè)錯(cuò)誤處理者要處理所有的錯(cuò)誤類型,這個(gè)假設(shè)在某些情況下是不成立的,比如:如果對于LogicError我們不打算通知LOGSys進(jìn)行處理會(huì)怎樣呢?我們不得不要寫一個(gè)處理該錯(cuò)誤的空函數(shù)(當(dāng)然你可以在ErrorHandler中寫一個(gè)缺省的實(shí)現(xiàn))。如果ErrorBase的類層次架構(gòu)越來越大,并且它們要求的處理方法也有很多的不同,就會(huì)導(dǎo)致ErrorHandler接口中的方法集龐大,并且任何一個(gè)ErrorBase的派生類的改變,都會(huì)導(dǎo)致大規(guī)模的重新編譯(甚至是毫無關(guān)系的類也要重新編譯),重新分布,如果這種改變比較頻繁,結(jié)果當(dāng)然是無法忍受的。
3、問題分析
上述的問題描述暴露了visitor模式的一些使用限制,即它僅僅適用于哪些受訪問的類層次架構(gòu)比較固定的情況,導(dǎo)致這樣的原因可以使用一個(gè)著名的面向?qū)ο笤O(shè)計(jì)原則來解釋,這個(gè)原則就是:DIP(Dependence Inversion Principle),這個(gè)原則的核心含義是:高層模塊不應(yīng)該直接依賴于低層模塊,所有的模塊都要依賴于抽象。也就是說:容易變化的東西一定要依賴于不容易變化的東西,因?yàn)槌橄蟮臇|西最不容易變化,具體的東西容易變化,所以具體應(yīng)該依賴于抽象。而在visitor模式中,ErrorHandler依賴于ErrorBase的所有的具體的派生類,并且如果這些派生類容易變化的話,就會(huì)導(dǎo)致不能接受的結(jié)果。
通過上面的分析,可以看出打斷這個(gè)循環(huán)依賴的環(huán)是克服visitor模式適用范圍限制的關(guān)鍵。
4、解決方案
在上述的循環(huán)依賴關(guān)系中,有兩段依賴關(guān)系是無法打斷的,一段是ErrorBase的所有派生類對于ErrorBase的依賴,一段是ErrorBase對于ErrorHandler的依賴,并且這兩段依賴關(guān)系也是符合DIP的,那么對于僅剩的一段依賴關(guān)系我們是必須要打斷的了,這段關(guān)系就是,ErrorHandler對于ErrorBase所有派生類的依賴,并且這段關(guān)系也是違反DIP的。如果我們不讓ErrorHandler知道ErrorBase的派生類,那么怎樣才能夠針對每一個(gè)具體的ErrorBase的派生類進(jìn)行相應(yīng)的處理呢?
面向?qū)ο蟠髱烺obert C. Martin給出了一個(gè)優(yōu)雅的解決方案,他所采用的技巧是OO方法所建議避免使用的,RTTI(運(yùn)行時(shí)類型鑒別)?梢娙绻鸕TTI運(yùn)用的得當(dāng),一樣可以得到很好的設(shè)計(jì)方案,并且還能夠克服一些OO中多態(tài)的方法解決不了的問題(當(dāng)然如果使用多態(tài)能夠解決的問題,推薦還是使用多態(tài)的方法進(jìn)行解決)。讓我們先看看這個(gè)解決方案的類圖:
通過上圖可以看出,ErrorHandler中沒有任何方法了,已經(jīng)退化為一個(gè)空的接口,所以也就不可能再依賴于任何ErrorBase的派生類了。和visitor模式不同,這個(gè)方案中針對每一個(gè)特定的ErrorBase的派生類定義一個(gè)相應(yīng)的處理接口,在每個(gè)派生類的handle方法實(shí)現(xiàn)中,運(yùn)用RTTI技術(shù)進(jìn)行相應(yīng)的類型轉(zhuǎn)換(把ErrorHandler轉(zhuǎn)換為自己對應(yīng)的錯(cuò)誤處理接口,比如:在DbError的handle方法中,就把ErrorHandler轉(zhuǎn)換為DbErrorHandler。Java在這方面做得不錯(cuò),可以進(jìn)行比較安全的類型轉(zhuǎn)換),想要處理該錯(cuò)誤的實(shí)體不僅要實(shí)現(xiàn)ErrorHandler接口,而且還要實(shí)現(xiàn)相應(yīng)的針對具體錯(cuò)誤類的處理接口,比如:GUISys就實(shí)現(xiàn)了三個(gè)接口(ErrorHandler、DBErrorHandler以及CommErrorHandler),從而也就實(shí)現(xiàn)了對DbError和CommError的處理。