使用設計模式改善程序結構(一)
? ? ? ? 設計模式是對特定問題經過無數次經驗總結后提出的能夠解決它的優雅的方案。但是,如果想要真正使設計模式發揮最大作用,僅僅知道設計模式是什么,以及它是如何實現的是很不夠的,因為那樣就不能使你對于設計模式有真正的理解,也就不能夠在自己的設計中正確、恰當的使用設計模式。本文試圖從另一個角度(設計模式的意圖、動機)來看待設計模式,通過這種新的思路,設計模式會變得非常貼近你的設計過程,并且能夠指導、簡化你的設計,最終將會導出一個優秀的解決方案。
1、介紹
在進行項目的開發活動中,有一些設計在項目剛剛開始工作的很好,但是隨著項目的進展,發現需要對已有的代碼進行修改或者擴展,導致這樣做的原因主要有:新的功能需求的需要以及對系統進一步理解。在這個時候,我們往往會發現進行這項工作比較困難,即使能完成也要付出很大的代價。此時,一個必須要做的工作就是要對現有的代碼進行重構(refactoring),通過重構使得我們接下來的工作變得相對容易。
重構就是在不改變軟件系統代碼的外部行為的前提下,改善它的內部結構。重構的目標就是使代碼結構更加合理,富有彈性,能夠適應新的需求、新的變化。對于特定問題給出優美解決方案的設計模式往往會成為重構的目標,而且一旦我們能夠識別出能夠解決我們問題的設計模式,將會大大簡化我們的工作,因為我們可以重用別人已經做過的工作。但是在我們的原始設計和最終可能會適用于我們的設計模式間的過渡并不是平滑的,而是有一個間隙。這樣的結果就是:即使我們已經知道了很多的設計模式,面對我們的實際問題,我們也沒有一個有效的方法去判斷哪一個設計模式適用于我們的系統,我們應該去怎樣應用它。
造成上述問題的原因往往是由于過于注重設計模式所給出的解決方案這個結果,而對于設計模式的意圖,以及它產生的動機卻忽略了。然而,正是設計模式的意圖、動機促使人們給出了一個解決一類問題的方案這個結果,設計模式的動機、意圖體現了該模式的形成思路,所以更加貼近我們的實際問題,從而會有效的指導我們的重構歷程。本文將通過一個實例來展示這個過程。
在本文中對例子進行了簡化,這樣做是為了突出問題的實質并且會使我們的思路更加清晰。思路本身才是最重要、最根本的,簡化了的例子不會降低我們所展示的思路、方法的適用性。
2、問題描述
一個完善的軟件系統,必須要對出現的錯誤進行相應的處理,只有這樣才能使系統足夠的健壯,我準備以軟件系統中對于錯誤的處理為例,來展示 我所使用的思路、方法。
在一個分布式的網管系統中,一個操作往往不會一定成功,常常會因為這樣或者那樣的原因失敗,此時我們就要根據失敗的原因相應的處理,使錯誤的影響局限在最小的范圍內,最好能夠恢復而不影響系統的正常運行,還有一點很重要,那就是在對錯誤進行處理的同時,一定不要忘記通知系統的管理者,因為只有管理者才有能力對錯誤進行進一步的分析,從而查找出錯誤的根源,從根本上解決錯誤。
下面我就從錯誤處理的通告管理者部分入手,開始我們的旅程。假定一個在一個分布式環境中訪問數據庫的操作,那么就有可能因為通信的原因或者數據庫本身的原因失敗,此時我們要通過用戶界面來通知管理者發生的錯誤。簡化了的代碼示例如下:
/* 錯誤碼定義 */ class ErrorConstant { public static final int ERROR_DBACCESS = 100; public static final int ERROR_COMMUNICATION = 101; } /* 省略了用戶界面中的其他的功能 */ class GUISys { public void announceError(int errCode) { switch(errCode) { case ErrorConstant.ERROR_DBACCESS: /* 通告管理者數據庫訪問錯誤的發生*/ break; case ErrorConstant.ERROR_COMMUNICATION: /* 通告管理者通信錯誤的發生*/ break; } } }
開始,這段代碼工作的很好,能夠完成我們需要的功能。但是這段代碼缺少相應的彈性,很難適應需求的變化。
3、問題分析
熟悉面向對象的讀者很快就會發現上面的代碼是典型的結構化的方法,結構化的方法是以具體的功能為核心來組織程序的結構,它的封裝度僅為1級,即僅有對于特定的功能的封裝(函數)。這使得結構化的方法很難適應需求的變化,面向對象的方法正是在這一點上優于結構化的方法。在面向對象領域,是以對象來組成程序結構的,一個對象有自己的職責,通過對象間的交互來完成系統的功能,這使得它的封裝度至少為2級,即封裝了為完成自己職責的方法和數據。另外面向對象的方法還支持更高層次的封裝,比如:通過對于不同的具體對象的共同的概念行為進行描述,我們可以達到3級的封裝度 - 抽象的類(在Java中就是接口)。封裝的層次越高,抽象的層次就越高,使得設計、代碼有越高的彈性,越容易適應變化。
考慮對上一節中的代碼,如果在系統的開發過程中發現需要對一種新的錯誤進行處理,比如:用戶認證錯誤,我們該如何做使得我們的系統能夠增加對于此項功能的需求呢?一種比較簡單、直接的做法就是在增加一條用來處理此項錯誤的case語句。是的,這種方法的確能夠工作,但是這樣做是要付出代價的。
首先,隨著系統的進一步開發,可能會出現更多的錯誤類型,那么就會導致對于錯誤的處理部分代碼冗長,不利于維護。其次,也是最根本的一點,修改已經能夠工作的代碼,很容易引入錯誤,并且在很多的情況下,錯誤都是在不經意下引入的,對于這種類型的錯誤很難定位。有調查表明,我們在開發過程中,用于修正錯誤的時間并不多,大部分的時間是在調試、發現錯誤。在面向對象領域,有一個很著名的原則:OCP(Open-Closed Principle),它的核心含意是:一個好的設計應該能夠容納新的功能需求的增加,但是增加的方式不是通過修改又有的模塊(類),而是通過增加新的模塊(類)來完成的。如果一個設計能夠遵循OCP,那么就能夠有效的避免上述的問題。
要是一個設計能夠符合OCP原則,就要求我們在進行設計時不能簡單的以功能為核心。要實現OCP的關鍵是抽象,抽象表征了一個固定的行為,但是對于這個行為可以有很多個不同的具體實現方法。通過抽象,我們就可以用一個固定的抽象的概念來代替哪些容易變化的數量眾多的具體的概念,并且使得原來依賴于哪些容易變化的概念的模塊,依賴于這個固定的抽象的概念,這樣的結果就是:系統新的需求的增加,僅僅會引起具體的概念的增加,而不會影響依賴于具體概念的抽象體的其他模塊。在實現的層面上,抽象體是通過抽象類來描述的,在Java中是接口(interface)。關于OCP的更詳細描述,請參見? 參考文獻[2] 。
既然知道了問題的本質以及相應的解決方法,下面就來改善我們的代碼結構。
4、初步方案
讓我們重新審視代碼,看看該如何進行抽象。在錯誤處理中,需要處理不同類型的錯誤,每個具體的錯誤具有特定于自己本身的一些信息,但是它們在概念層面上又是一致的,比如:都可以通過特定的方法接口獲取自已內部的錯誤信息,每一個錯誤都有自己的處理方法。由此可以得到一個初步的方案:可以定義一個抽象的錯誤基類,在這個基類里面定義一些在概念上適用于所有不同的具體錯誤的方法,每個具體的錯誤可以有自己的不同的對于這些方法的實現。代碼示例如下:
interface ErrorBase { public void handle(); public String getInfo(); } class DBAccessError implements ErrorBase { public void handle() { /* 進行關于數據庫訪問錯誤的處理 */ } public String getInfo() { /* 構造返回關于數據庫訪問錯誤的信息 */ } } class CommunicationError implements ErrorBase { public void handle() { /* 進行關于通信錯誤的處理 */ } public String getInfo() { /* 構造返回關于通信錯誤的信息 */ } }
這樣,我們就可以在錯誤發生處,構造一個實際的錯誤對象,并以ErrorBase引用它。然后,交給給錯誤處理模塊,此時錯誤處理模塊就僅僅知道一個類型ErrorBase,而無需知道每一個具體的錯誤類型,這樣就可以使用統一的方式來處理錯誤了。代碼示例如下:
class GUISys { public void announceError(ErrorBase error) { /* 使用一致的方式處理錯誤 */ error.handle(); } }
可以看出,對于新的錯誤類型的增加,僅僅需要增加一個具體的錯誤類,對于錯誤處理部分沒有任何影響??瓷先ズ芡昝?,也符合OCP原則,但是進一步分析就會發現,這個方案一樣存在著問題,我們將在下一個小節進行詳細的說明。
5、進一步分析
上一個小節給出了一個方案,對于只有GUISys這一個錯誤處理者是很完美的,但是情況往往不是這樣的。前面也曾經提到過,對于發生的錯誤,除了要通知系統的使用者外,還要進行其他的處理,比如:試圖恢復,記如日志等??梢钥闯?,這些處理方法和把錯誤通告給使用者是非常不同的,完全沒有辦法僅僅用一個handle方法來統一所有的不同的處理。但是,如果我們在ErrorBase中增加不同的處理方法聲明,在具體的錯誤類中,根據自身的需要來相應的實現這些方法,好像也是一個不錯的方案。代碼示例如下:
interface ErrorBase { public void guiHandle(); public void logHandle(); } class DBAccessError implements ErrorBase { public void guiHandle() { /* 通知用戶界面的數據庫訪問錯誤處理 */ } public void logHandle() { /* 通知日志系統的數據庫訪問錯誤處理 */ } } class CommunicationError implements ErrorBase { public void guiHandle() { /* 通知用戶界面的通信錯誤處理 */ } public void logHandle() { /* 通知日志系統的通信錯誤處理 */ } } class GUISys { public void announceError(ErrorBase error) { error.guiHandle(); } } class LogSys { public void announceError(ErrorBase error) { error.logHandle(); } }
讀者可能已經注意到,這種做法其實也不是十分符合OCP,雖然它把變化局限在ErrorBase這個類層次架構中,但是增加新的處理方法,還是更改了已經存在的ErrorBase類。其實,這種設計方法,還違反了另外一個著名的面向對象的設計原則:SRP(Single Responsibility Principle)。這個原則的核心含意是:一個類應該有且僅有一個職責。關于職責的含意,面向對象大師Robert.C Martin有一個著名的定義:所謂一個類的職責是指引起該類變化的原因,如果一個類具有一個以上的職責,那么就會有多個不同的原因引起該類變化,其實就是耦合了多個互不相關的職責,就會降低這個類的內聚性。錯誤類的職責就是,保存和自己相關的錯誤狀態,并且提供方法用于獲取這些狀態。上面的設計中把不同的處理方法也放到錯誤類中,從而增加了錯誤類的職責,這樣即使和錯誤類本身沒有關系的對于錯誤處理方式的變化,增加、修改都會導致錯誤類的修改。這種設計方法一樣會在需求變化時,帶來沒有預料到的問題。那么能否將對錯誤的處理方法從中剝離出來呢?如果讀者比較熟悉設計模式(這里的熟悉是指,設計模式的意圖、動機,而不是指怎樣去實現一個具體的設計模式),應該會隱隱約約感覺到一個更好的設計方案即將出現。
6、設計模式浮出水面
讓我們對問題重新描述一下:我們已經有了一個關于錯誤的類層次結構,現在我們需要在不改變這個類層次結構的前提下允許我們增加對于這個類層次的新的處理方法。聽起來很耳熟吧,不錯,這正是過于visitor設計模式的意圖的描述。通過對于該模式動機的分析,我們很容易知道,要想使用visitor模式,需要定義兩個類層次:一個對應于接收操作的元素的類層次(就是我們的錯誤類),另一個對應于定義對元素的操作的訪問者(就是我們的對于錯誤的不同處理方法)。這樣,我們就轉換了問題視角,即把需要不同的錯誤處理方法的問題轉變為需要不同的錯誤處理類,這樣的結果就是我們可以通過增加新的模塊(類)來增加新的錯誤處理方法,而不是通過增加新的錯誤處理方法(這樣做,就勢必要修改已經存在的類)。
一旦到了這一部,下面的工作就比較簡單了,因為visitor模式已經為我們搭建了一個設計的上下文,此時我們就可以關注visitor模式的實現部分來指導我們下面的具體實現了。下面僅僅給出最終的程序結構的UML圖以及代碼示例,其中忽略了錯誤類中的屬于錯誤本身的方法,各個具體的錯誤處理方法通過這些方法和具體的錯誤類對象交互,來完成各自的處理功能。
最終的設計的程序結構圖
最終的代碼示例
interface ErrorBase { public void handle(ErrorHandler handler); } class DBError implements ErrorBase { public void handle(ErrorHandler handler) { handler.handle(this); } } class CommError implements ErrorBase { public void handle(ErrorHandler handler) { handler.handle(this); } } interface ErrorHandler { public void handle(DBrror dbError); public void handle(CommError commError); } class GUISys implements ErrorHandler { public void announceError(ErrorBase error) { error.handle(this); } public void handle(DBError dbError) { /* 通知用戶界面進行有關數據庫錯誤的處理 */ } public void handle(CommError commError) { /* 通知用戶界面進行有關通信錯誤的處理 */ } } class LogSys implements ErrorHandler { public void announceError(ErrorBase error) { error.handle(this); } public void handle(DBError dbError) { /* 通知日志系統進行有關數據庫錯誤的處理 */ } public void handle(CommError commError) { /* 通知日志系統進行有關通信錯誤的處理 */ } }
7、結論
設計模式并不僅僅是一個有關特定問題的解決方案這個結果,它的意圖以及它的動機往往更重要,因為一旦我們理解了一個設計模式的意圖、動機,那么在設計過程中,就很容易的發現適用于我們自己的設計模式,從而大大簡化設計工作,并且可以得到一個比較理想的設計方案。
另外,在學習設計模式的過程中,應該更加注意設計模式背后的東西,即具體設計模式所共有的的一些優秀的指導原則,這些原則在? 參考文獻[1] 的第一章中有詳細的論述,基本上有兩點:
- 發現變化,封裝變化
- 優先使用組合(Composition),而不是繼承
如果注意從這些方面來學習、理解設計模式,就會得到一些比單個具體設計模式本身更有用的知識,并且即使在沒有現成模式可用的情況下,我們也一樣可以設計出一個好的系統來。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
