使用設(shè)計(jì)模式改善程序結(jié)構(gòu)(一)
? ? ? ? 設(shè)計(jì)模式是對特定問題經(jīng)過無數(shù)次經(jīng)驗(yàn)總結(jié)后提出的能夠解決它的優(yōu)雅的方案。但是,如果想要真正使設(shè)計(jì)模式發(fā)揮最大作用,僅僅知道設(shè)計(jì)模式是什么,以及它是如何實(shí)現(xiàn)的是很不夠的,因?yàn)槟菢泳筒荒苁鼓銓τ谠O(shè)計(jì)模式有真正的理解,也就不能夠在自己的設(shè)計(jì)中正確、恰當(dāng)?shù)氖褂迷O(shè)計(jì)模式。本文試圖從另一個(gè)角度(設(shè)計(jì)模式的意圖、動機(jī))來看待設(shè)計(jì)模式,通過這種新的思路,設(shè)計(jì)模式會變得非常貼近你的設(shè)計(jì)過程,并且能夠指導(dǎo)、簡化你的設(shè)計(jì),最終將會導(dǎo)出一個(gè)優(yōu)秀的解決方案。
1、介紹
在進(jìn)行項(xiàng)目的開發(fā)活動中,有一些設(shè)計(jì)在項(xiàng)目剛剛開始工作的很好,但是隨著項(xiàng)目的進(jìn)展,發(fā)現(xiàn)需要對已有的代碼進(jìn)行修改或者擴(kuò)展,導(dǎo)致這樣做的原因主要有:新的功能需求的需要以及對系統(tǒng)進(jìn)一步理解。在這個(gè)時(shí)候,我們往往會發(fā)現(xiàn)進(jìn)行這項(xiàng)工作比較困難,即使能完成也要付出很大的代價(jià)。此時(shí),一個(gè)必須要做的工作就是要對現(xiàn)有的代碼進(jìn)行重構(gòu)(refactoring),通過重構(gòu)使得我們接下來的工作變得相對容易。
重構(gòu)就是在不改變軟件系統(tǒng)代碼的外部行為的前提下,改善它的內(nèi)部結(jié)構(gòu)。重構(gòu)的目標(biāo)就是使代碼結(jié)構(gòu)更加合理,富有彈性,能夠適應(yīng)新的需求、新的變化。對于特定問題給出優(yōu)美解決方案的設(shè)計(jì)模式往往會成為重構(gòu)的目標(biāo),而且一旦我們能夠識別出能夠解決我們問題的設(shè)計(jì)模式,將會大大簡化我們的工作,因?yàn)槲覀兛梢灾赜脛e人已經(jīng)做過的工作。但是在我們的原始設(shè)計(jì)和最終可能會適用于我們的設(shè)計(jì)模式間的過渡并不是平滑的,而是有一個(gè)間隙。這樣的結(jié)果就是:即使我們已經(jīng)知道了很多的設(shè)計(jì)模式,面對我們的實(shí)際問題,我們也沒有一個(gè)有效的方法去判斷哪一個(gè)設(shè)計(jì)模式適用于我們的系統(tǒng),我們應(yīng)該去怎樣應(yīng)用它。
造成上述問題的原因往往是由于過于注重設(shè)計(jì)模式所給出的解決方案這個(gè)結(jié)果,而對于設(shè)計(jì)模式的意圖,以及它產(chǎn)生的動機(jī)卻忽略了。然而,正是設(shè)計(jì)模式的意圖、動機(jī)促使人們給出了一個(gè)解決一類問題的方案這個(gè)結(jié)果,設(shè)計(jì)模式的動機(jī)、意圖體現(xiàn)了該模式的形成思路,所以更加貼近我們的實(shí)際問題,從而會有效的指導(dǎo)我們的重構(gòu)歷程。本文將通過一個(gè)實(shí)例來展示這個(gè)過程。
在本文中對例子進(jìn)行了簡化,這樣做是為了突出問題的實(shí)質(zhì)并且會使我們的思路更加清晰。思路本身才是最重要、最根本的,簡化了的例子不會降低我們所展示的思路、方法的適用性。
2、問題描述
一個(gè)完善的軟件系統(tǒng),必須要對出現(xiàn)的錯(cuò)誤進(jìn)行相應(yīng)的處理,只有這樣才能使系統(tǒng)足夠的健壯,我準(zhǔn)備以軟件系統(tǒng)中對于錯(cuò)誤的處理為例,來展示 我所使用的思路、方法。
在一個(gè)分布式的網(wǎng)管系統(tǒng)中,一個(gè)操作往往不會一定成功,常常會因?yàn)檫@樣或者那樣的原因失敗,此時(shí)我們就要根據(jù)失敗的原因相應(yīng)的處理,使錯(cuò)誤的影響局限在最小的范圍內(nèi),最好能夠恢復(fù)而不影響系統(tǒng)的正常運(yùn)行,還有一點(diǎn)很重要,那就是在對錯(cuò)誤進(jìn)行處理的同時(shí),一定不要忘記通知系統(tǒng)的管理者,因?yàn)橹挥泄芾碚卟庞心芰﹀e(cuò)誤進(jìn)行進(jìn)一步的分析,從而查找出錯(cuò)誤的根源,從根本上解決錯(cuò)誤。
下面我就從錯(cuò)誤處理的通告管理者部分入手,開始我們的旅程。假定一個(gè)在一個(gè)分布式環(huán)境中訪問數(shù)據(jù)庫的操作,那么就有可能因?yàn)橥ㄐ诺脑蚧蛘邤?shù)據(jù)庫本身的原因失敗,此時(shí)我們要通過用戶界面來通知管理者發(fā)生的錯(cuò)誤。簡化了的代碼示例如下:
/* 錯(cuò)誤碼定義 */
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:
/* 通告管理者數(shù)據(jù)庫訪問錯(cuò)誤的發(fā)生*/
break;
case ErrorConstant.ERROR_COMMUNICATION:
/* 通告管理者通信錯(cuò)誤的發(fā)生*/
break;
}
}
}
開始,這段代碼工作的很好,能夠完成我們需要的功能。但是這段代碼缺少相應(yīng)的彈性,很難適應(yīng)需求的變化。
3、問題分析
熟悉面向?qū)ο蟮淖x者很快就會發(fā)現(xiàn)上面的代碼是典型的結(jié)構(gòu)化的方法,結(jié)構(gòu)化的方法是以具體的功能為核心來組織程序的結(jié)構(gòu),它的封裝度僅為1級,即僅有對于特定的功能的封裝(函數(shù))。這使得結(jié)構(gòu)化的方法很難適應(yīng)需求的變化,面向?qū)ο蟮姆椒ㄕ窃谶@一點(diǎn)上優(yōu)于結(jié)構(gòu)化的方法。在面向?qū)ο箢I(lǐng)域,是以對象來組成程序結(jié)構(gòu)的,一個(gè)對象有自己的職責(zé),通過對象間的交互來完成系統(tǒng)的功能,這使得它的封裝度至少為2級,即封裝了為完成自己職責(zé)的方法和數(shù)據(jù)。另外面向?qū)ο蟮姆椒ㄟ€支持更高層次的封裝,比如:通過對于不同的具體對象的共同的概念行為進(jìn)行描述,我們可以達(dá)到3級的封裝度 - 抽象的類(在Java中就是接口)。封裝的層次越高,抽象的層次就越高,使得設(shè)計(jì)、代碼有越高的彈性,越容易適應(yīng)變化。
考慮對上一節(jié)中的代碼,如果在系統(tǒng)的開發(fā)過程中發(fā)現(xiàn)需要對一種新的錯(cuò)誤進(jìn)行處理,比如:用戶認(rèn)證錯(cuò)誤,我們該如何做使得我們的系統(tǒng)能夠增加對于此項(xiàng)功能的需求呢?一種比較簡單、直接的做法就是在增加一條用來處理此項(xiàng)錯(cuò)誤的case語句。是的,這種方法的確能夠工作,但是這樣做是要付出代價(jià)的。
首先,隨著系統(tǒng)的進(jìn)一步開發(fā),可能會出現(xiàn)更多的錯(cuò)誤類型,那么就會導(dǎo)致對于錯(cuò)誤的處理部分代碼冗長,不利于維護(hù)。其次,也是最根本的一點(diǎn),修改已經(jīng)能夠工作的代碼,很容易引入錯(cuò)誤,并且在很多的情況下,錯(cuò)誤都是在不經(jīng)意下引入的,對于這種類型的錯(cuò)誤很難定位。有調(diào)查表明,我們在開發(fā)過程中,用于修正錯(cuò)誤的時(shí)間并不多,大部分的時(shí)間是在調(diào)試、發(fā)現(xiàn)錯(cuò)誤。在面向?qū)ο箢I(lǐng)域,有一個(gè)很著名的原則:OCP(Open-Closed Principle),它的核心含意是:一個(gè)好的設(shè)計(jì)應(yīng)該能夠容納新的功能需求的增加,但是增加的方式不是通過修改又有的模塊(類),而是通過增加新的模塊(類)來完成的。如果一個(gè)設(shè)計(jì)能夠遵循OCP,那么就能夠有效的避免上述的問題。
要是一個(gè)設(shè)計(jì)能夠符合OCP原則,就要求我們在進(jìn)行設(shè)計(jì)時(shí)不能簡單的以功能為核心。要實(shí)現(xiàn)OCP的關(guān)鍵是抽象,抽象表征了一個(gè)固定的行為,但是對于這個(gè)行為可以有很多個(gè)不同的具體實(shí)現(xiàn)方法。通過抽象,我們就可以用一個(gè)固定的抽象的概念來代替哪些容易變化的數(shù)量眾多的具體的概念,并且使得原來依賴于哪些容易變化的概念的模塊,依賴于這個(gè)固定的抽象的概念,這樣的結(jié)果就是:系統(tǒng)新的需求的增加,僅僅會引起具體的概念的增加,而不會影響依賴于具體概念的抽象體的其他模塊。在實(shí)現(xiàn)的層面上,抽象體是通過抽象類來描述的,在Java中是接口(interface)。關(guān)于OCP的更詳細(xì)描述,請參見? 參考文獻(xiàn)[2] 。
既然知道了問題的本質(zhì)以及相應(yīng)的解決方法,下面就來改善我們的代碼結(jié)構(gòu)。
4、初步方案
讓我們重新審視代碼,看看該如何進(jìn)行抽象。在錯(cuò)誤處理中,需要處理不同類型的錯(cuò)誤,每個(gè)具體的錯(cuò)誤具有特定于自己本身的一些信息,但是它們在概念層面上又是一致的,比如:都可以通過特定的方法接口獲取自已內(nèi)部的錯(cuò)誤信息,每一個(gè)錯(cuò)誤都有自己的處理方法。由此可以得到一個(gè)初步的方案:可以定義一個(gè)抽象的錯(cuò)誤基類,在這個(gè)基類里面定義一些在概念上適用于所有不同的具體錯(cuò)誤的方法,每個(gè)具體的錯(cuò)誤可以有自己的不同的對于這些方法的實(shí)現(xiàn)。代碼示例如下:
interface ErrorBase
{
public void handle();
public String getInfo();
}
class DBAccessError implements ErrorBase
{
public void handle() {
/* 進(jìn)行關(guān)于數(shù)據(jù)庫訪問錯(cuò)誤的處理 */
}
public String getInfo() {
/* 構(gòu)造返回關(guān)于數(shù)據(jù)庫訪問錯(cuò)誤的信息 */
}
}
class CommunicationError implements ErrorBase
{
public void handle() {
/* 進(jìn)行關(guān)于通信錯(cuò)誤的處理 */
}
public String getInfo() {
/* 構(gòu)造返回關(guān)于通信錯(cuò)誤的信息 */
}
}
這樣,我們就可以在錯(cuò)誤發(fā)生處,構(gòu)造一個(gè)實(shí)際的錯(cuò)誤對象,并以ErrorBase引用它。然后,交給給錯(cuò)誤處理模塊,此時(shí)錯(cuò)誤處理模塊就僅僅知道一個(gè)類型ErrorBase,而無需知道每一個(gè)具體的錯(cuò)誤類型,這樣就可以使用統(tǒng)一的方式來處理錯(cuò)誤了。代碼示例如下:
class GUISys
{
public void announceError(ErrorBase error) {
/* 使用一致的方式處理錯(cuò)誤 */
error.handle();
}
}
可以看出,對于新的錯(cuò)誤類型的增加,僅僅需要增加一個(gè)具體的錯(cuò)誤類,對于錯(cuò)誤處理部分沒有任何影響。看上去很完美,也符合OCP原則,但是進(jìn)一步分析就會發(fā)現(xiàn),這個(gè)方案一樣存在著問題,我們將在下一個(gè)小節(jié)進(jìn)行詳細(xì)的說明。
5、進(jìn)一步分析
上一個(gè)小節(jié)給出了一個(gè)方案,對于只有GUISys這一個(gè)錯(cuò)誤處理者是很完美的,但是情況往往不是這樣的。前面也曾經(jīng)提到過,對于發(fā)生的錯(cuò)誤,除了要通知系統(tǒng)的使用者外,還要進(jìn)行其他的處理,比如:試圖恢復(fù),記如日志等。可以看出,這些處理方法和把錯(cuò)誤通告給使用者是非常不同的,完全沒有辦法僅僅用一個(gè)handle方法來統(tǒng)一所有的不同的處理。但是,如果我們在ErrorBase中增加不同的處理方法聲明,在具體的錯(cuò)誤類中,根據(jù)自身的需要來相應(yīng)的實(shí)現(xiàn)這些方法,好像也是一個(gè)不錯(cuò)的方案。代碼示例如下:
interface ErrorBase
{
public void guiHandle();
public void logHandle();
}
class DBAccessError implements ErrorBase
{
public void guiHandle() {
/* 通知用戶界面的數(shù)據(jù)庫訪問錯(cuò)誤處理 */
}
public void logHandle() {
/* 通知日志系統(tǒng)的數(shù)據(jù)庫訪問錯(cuò)誤處理 */
}
}
class CommunicationError implements ErrorBase
{
public void guiHandle() {
/* 通知用戶界面的通信錯(cuò)誤處理 */
}
public void logHandle() {
/* 通知日志系統(tǒng)的通信錯(cuò)誤處理 */
}
}
class GUISys
{
public void announceError(ErrorBase error) {
error.guiHandle();
}
}
class LogSys
{
public void announceError(ErrorBase error) {
error.logHandle();
}
}
讀者可能已經(jīng)注意到,這種做法其實(shí)也不是十分符合OCP,雖然它把變化局限在ErrorBase這個(gè)類層次架構(gòu)中,但是增加新的處理方法,還是更改了已經(jīng)存在的ErrorBase類。其實(shí),這種設(shè)計(jì)方法,還違反了另外一個(gè)著名的面向?qū)ο蟮脑O(shè)計(jì)原則:SRP(Single Responsibility Principle)。這個(gè)原則的核心含意是:一個(gè)類應(yīng)該有且僅有一個(gè)職責(zé)。關(guān)于職責(zé)的含意,面向?qū)ο蟠髱烺obert.C Martin有一個(gè)著名的定義:所謂一個(gè)類的職責(zé)是指引起該類變化的原因,如果一個(gè)類具有一個(gè)以上的職責(zé),那么就會有多個(gè)不同的原因引起該類變化,其實(shí)就是耦合了多個(gè)互不相關(guān)的職責(zé),就會降低這個(gè)類的內(nèi)聚性。錯(cuò)誤類的職責(zé)就是,保存和自己相關(guān)的錯(cuò)誤狀態(tài),并且提供方法用于獲取這些狀態(tài)。上面的設(shè)計(jì)中把不同的處理方法也放到錯(cuò)誤類中,從而增加了錯(cuò)誤類的職責(zé),這樣即使和錯(cuò)誤類本身沒有關(guān)系的對于錯(cuò)誤處理方式的變化,增加、修改都會導(dǎo)致錯(cuò)誤類的修改。這種設(shè)計(jì)方法一樣會在需求變化時(shí),帶來沒有預(yù)料到的問題。那么能否將對錯(cuò)誤的處理方法從中剝離出來呢?如果讀者比較熟悉設(shè)計(jì)模式(這里的熟悉是指,設(shè)計(jì)模式的意圖、動機(jī),而不是指怎樣去實(shí)現(xiàn)一個(gè)具體的設(shè)計(jì)模式),應(yīng)該會隱隱約約感覺到一個(gè)更好的設(shè)計(jì)方案即將出現(xiàn)。
6、設(shè)計(jì)模式浮出水面
讓我們對問題重新描述一下:我們已經(jīng)有了一個(gè)關(guān)于錯(cuò)誤的類層次結(jié)構(gòu),現(xiàn)在我們需要在不改變這個(gè)類層次結(jié)構(gòu)的前提下允許我們增加對于這個(gè)類層次的新的處理方法。聽起來很耳熟吧,不錯(cuò),這正是過于visitor設(shè)計(jì)模式的意圖的描述。通過對于該模式動機(jī)的分析,我們很容易知道,要想使用visitor模式,需要定義兩個(gè)類層次:一個(gè)對應(yīng)于接收操作的元素的類層次(就是我們的錯(cuò)誤類),另一個(gè)對應(yīng)于定義對元素的操作的訪問者(就是我們的對于錯(cuò)誤的不同處理方法)。這樣,我們就轉(zhuǎn)換了問題視角,即把需要不同的錯(cuò)誤處理方法的問題轉(zhuǎn)變?yōu)樾枰煌腻e(cuò)誤處理類,這樣的結(jié)果就是我們可以通過增加新的模塊(類)來增加新的錯(cuò)誤處理方法,而不是通過增加新的錯(cuò)誤處理方法(這樣做,就勢必要修改已經(jīng)存在的類)。
一旦到了這一部,下面的工作就比較簡單了,因?yàn)関isitor模式已經(jīng)為我們搭建了一個(gè)設(shè)計(jì)的上下文,此時(shí)我們就可以關(guān)注visitor模式的實(shí)現(xiàn)部分來指導(dǎo)我們下面的具體實(shí)現(xiàn)了。下面僅僅給出最終的程序結(jié)構(gòu)的UML圖以及代碼示例,其中忽略了錯(cuò)誤類中的屬于錯(cuò)誤本身的方法,各個(gè)具體的錯(cuò)誤處理方法通過這些方法和具體的錯(cuò)誤類對象交互,來完成各自的處理功能。
最終的設(shè)計(jì)的程序結(jié)構(gòu)圖
最終的代碼示例
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) {
/* 通知用戶界面進(jìn)行有關(guān)數(shù)據(jù)庫錯(cuò)誤的處理 */
}
public void handle(CommError commError) {
/* 通知用戶界面進(jìn)行有關(guān)通信錯(cuò)誤的處理 */
}
}
class LogSys implements ErrorHandler
{
public void announceError(ErrorBase error) {
error.handle(this);
}
public void handle(DBError dbError) {
/* 通知日志系統(tǒng)進(jìn)行有關(guān)數(shù)據(jù)庫錯(cuò)誤的處理 */
}
public void handle(CommError commError) {
/* 通知日志系統(tǒng)進(jìn)行有關(guān)通信錯(cuò)誤的處理 */
}
}
7、結(jié)論
設(shè)計(jì)模式并不僅僅是一個(gè)有關(guān)特定問題的解決方案這個(gè)結(jié)果,它的意圖以及它的動機(jī)往往更重要,因?yàn)橐坏┪覀兝斫饬艘粋€(gè)設(shè)計(jì)模式的意圖、動機(jī),那么在設(shè)計(jì)過程中,就很容易的發(fā)現(xiàn)適用于我們自己的設(shè)計(jì)模式,從而大大簡化設(shè)計(jì)工作,并且可以得到一個(gè)比較理想的設(shè)計(jì)方案。
另外,在學(xué)習(xí)設(shè)計(jì)模式的過程中,應(yīng)該更加注意設(shè)計(jì)模式背后的東西,即具體設(shè)計(jì)模式所共有的的一些優(yōu)秀的指導(dǎo)原則,這些原則在? 參考文獻(xiàn)[1] 的第一章中有詳細(xì)的論述,基本上有兩點(diǎn):
- 發(fā)現(xiàn)變化,封裝變化
- 優(yōu)先使用組合(Composition),而不是繼承
如果注意從這些方面來學(xué)習(xí)、理解設(shè)計(jì)模式,就會得到一些比單個(gè)具體設(shè)計(jì)模式本身更有用的知識,并且即使在沒有現(xiàn)成模式可用的情況下,我們也一樣可以設(shè)計(jì)出一個(gè)好的系統(tǒng)來。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

