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