3. 可變更性
前面我提到了,軟件的變更性是所有軟件理論的核心,那么什么是軟件的可變更性呢?按照現在的軟件理論,客戶對軟件的需求時時刻刻在發生著變化。當軟件設計好以后,為應對客戶需求的變更而進行的代碼修改,其所需要付出的代價,就是軟件設計的可變更性。由于軟件合理地設計,修改所付出的代價越小,則軟件的可變更性越好,即代碼設計的質量越高。一種非常理想的狀態是,無論客戶需求怎樣變化,軟件只需進行適當地修改就能夠適應。但這之所以稱之為理想狀態,因為客戶需求變化是有大有小的。如果客戶需求變化非常大,即使再好的設計也無法應付,甚至重新開發。然而,客戶需求的適當變化,一個合理地設計可以使得變更代價最小化,延續我們設計的軟件的生命力。
?
?
1 )通過提高代碼復用提高可維護性
我曾經遇到過這樣一件事,我要維護的一個系統因為應用范圍的擴大,它對機關級次的計算方式需要改變一種策略。如果這個項目統一采用一段公用方法來計算機關級次,這樣一個修改實在太簡單了,就是修改這個公用方法即可。但是,事實卻不一樣,對機關級次計算的代碼遍布整個項目,甚至有些還寫入到了那些復雜的 SQL 語句中。在這樣一種情況下,這樣一個需求的修改無異于需要遍歷這個項目代碼。這樣一個實例顯示了一個項目代碼復用的重要,然而不幸的是,代碼無法很好復用的情況遍布我們所有的項目。代碼復用的道理十分簡單,但要具體運作起來非常復雜,它除了需要很好的代碼規劃,還需要持續地代碼重構。
對整個系統的整體分析與合理規劃可以根本地保證代碼復用。系統分析師通過用例模型、領域模型、分析模型的一步一步分析,最后通過正向工程,生成系統需要設計的各種類及其各自的屬性和方法。采用這種方法,功能被合理地劃分到這個類中,可以很好地保證代碼復用。
采用以上方法雖然好,但技術難度較高,需要有高深的系統分析師,并不是所有項目都能普遍采用的,特別是時間比較緊張的項目。通過開發人員在設計過程中的重構,也許更加實用。當某個開發人員在開發一段代碼時,發現該功能與前面已經開發功能相同,或者部分相同。這時,這個開發人員可以對前面已經開發的功能進行重構,將可以通用的代碼提取出來,進行相應地改造,使其具有一定的通用性,便于各個地方可以使用。
一些比較成功的項目組會指定一個專門管理通用代碼的人,負責收集和整理項目組中各個成員編寫的,可以通用的代碼。這個負責人同時也應當具有一定的代碼編寫功力,因為將專用代碼提升為通用代碼,或者以前使用該通用代碼的某個功能,由于業務變更,而對這個通用代碼的變更要求,都對這個負責人提出了很高的能力要求。
雖然后一種方式非常實用,但是它有些亡羊補牢的味道,不能從整體上對項目代碼進行有效規劃。正因為兩種方法各有利弊,因此在項目中應當配合使用。
?
?
2 )利用設計模式提高可變更性
對于初學者,軟件設計理論常常感覺晦澀難懂。一個快速提高軟件質量的捷徑就是利用設計模式。這里說的設計模式,不僅僅指經典的 32 個模式,是一切前人總結的,我們可以利用的、更加廣泛的設計模式。
?
a. if...else...
這個我也不知道叫什么名字,最早是哪位大師總結的,它出現在 Larman 的《 UML 與模式應用》,也出現在出現在 Mardin 的《敏捷軟件開發》。它是這樣描述的:當你發現你必須要設計這樣的代碼:“ if...elseif...elseif...else... ”時,你應當想到你的代碼應當重構一下了。我們先看看這樣的代碼有怎樣的特點。
if(var.equals("A")){ doA(); } else if(var.equals("B")){ doB(); } else if(var.equals("C")){ doC(); } else{ doD(); }
?
?
?
這樣的代碼很常見,也非常平常,我們大家都寫過。但正是這樣平常才隱藏著我們永遠沒有注意的問題。問題就在于,如果某一天這個選項不再僅僅是 A 、 B 、 C ,而是增加了新的選項,會怎樣呢?你也許會說,那沒有關系,我把代碼改改就行。然而事實上并非如此,在大型軟件研發與維護中有一個原則,每次的變更盡量不要去修改原有的代碼。如果我們重構一下,能保證不修改原有代碼,僅僅增加新的代碼就能應付選項的增加,這就增加了這段代碼的可維護性和可變更性,提高了代碼質量。那么,我們應當如何去做呢?
?
經過深入分析你會發現,這里存在一個對應關系,即 A 對應 doA() , B 對應 doB() ...如果將 doA() 、 doB() 、 doC() ...與原有代碼解耦,問題就解決了。如何解耦呢?設計一個接口 X 以及它的實現 A 、 B 、 C ...每個類都包含一個方法 doX() ,并且將 doA() 的代碼放到 A.doX() 中,將 doB() 的代碼放到 B.doX() 中...經過以上的重構,代碼還是這些代碼,效果卻完全不一樣了。我們只需要這樣寫:
X x = factory.getBean(var); x.doX();
?
?
?
這樣就可以實現以上的功能了。我們看到這里有一個工廠,放著所有的 A 、 B 、 C ...并且與它們的 key 對應起來,并且寫在配置文件中。如果出現新的選項時,通過修改配置文件就可以無限制的增加下去。
這個模式雖然有效提高了代碼質量,但是不能濫用,并非只要出現 if...else... 就需要使用。由于它使用了工廠,一定程度上增加了代碼復雜度,因此僅僅在選項較多,并且增加選項的可能性很大的情況下才可以使用。另外,要使用這個模式,繼承我在附件中提供的抽象類 XmlBuildFactoryFacade 就可以快速建立一個工廠。如果你的項目放在 spring 或其它可配置框架中,也可以快速建立工廠。設計一個 Map 靜態屬性并使其 V 為這些 A 、 B 、 C ...這個工廠就建立起來了。
?
b. 策略模式
也許你看過策略模式( strategy model )的相關資料但沒有留下太多的印象。一個簡單的例子可以讓你快速理解它。如果一個員工系統中,員工被分為臨時工和正式工并且在不同的地方相應的行為不一樣。在設計它們的時候,你肯定設計一個抽象的員工類,并且設計兩個繼承類:臨時工和正式工。這樣,通過下塑類型,可以在不同的地方表現出臨時工和正式工的各自行為。在另一個系統中,員工被分為了銷售人員、技術人員、管理人員并且也在不同的地方相應的行為不一樣。同樣,我們在設計時也是設計一個抽象的員工類,并且設計數個繼承類:銷售人員、技術人員、管理人員。現在,我們要把這兩個系統合并起來,也就是說,在新的系統中,員工既被分為臨時工和正式工,又被分為了銷售人員、技術人員、管理人員,這時候如何設計。如果我們還是使用以往的設計,我們將不得不設計很多繼承類:銷售臨時工、銷售正式工、技術臨時工、技術正式工...如此的設計,在隨著劃分的類型,以及每種類型的選項的增多,呈笛卡爾增長。通過以上一個系統的設計,我們不得不發現,我們以往學習的關于繼承的設計遇到了挑戰。
解決繼承出現的問題,有一個最好的辦法,就是采用策略模式。在這個應用中,員工之所以要分為臨時工和正式工,無非是因為它們的一些行為不一樣,比如,發工資時的計算方式不同。如果我們在設計時不將員工類分為臨時工類和正式工類,而僅僅只有員工類,只是在類中增加“工資發放策略”。當我們創建員工對象時,根據員工的類型,將“工資發放策略”設定為“臨時工策略”或“正式工策略”,在計算工資時,只需要調用策略類中的“計算工資”方法,其行為的表現,也設計臨時工類和正式工類是一樣的。同樣的設計可以放到銷售人員策略、技術人員策略、管理人員策略中。一個通常的設計是,我們將某一個影響更大的、或者選項更少的屬性設計成繼承類,而將其它屬性設計成策略類,就可以很好的解決以上問題。
?
?
?
使用策略模式,你同樣把代碼寫活了,因為你可以無限制地增加策略。但是,使用策略模式你同樣需要設計一個工廠——策略工廠。以上實例中,你需要設計一個發放工資策略工廠,并且在工廠中將“臨時工”與“臨時工策略”對應起來,將“正式工”與“正式工策略”對應起來。
?
c. 適配器模式
我的筆記本是港貨,它的插頭與我們常用的插座不一樣,所有我出差的時候我必須帶一個適配器,才能使用不同地方的插座。這是一個對適配器模式最經典的描述。當我們設計的系統要與其它系統交互,或者我們設計的模塊要與其它模塊交互時,這種交互可能是調用一個接口,或者交換一段數據,接受方常常因發送方對協議的變更而頻繁變更。這種變更,可能是接受方來源的變更,比如原來是 A 系統,現在變成 B 系統了;也可能是接受方自身的代碼變更,如原來的接口現在增加了一個參數。由于發送方的變更常常導致接受方代碼的不穩定,即頻繁跟著修改,為接受方的維護帶來困難。
遇到這樣的問題,一個有經驗的程序員馬上想到的就是采用適配器模式。在設計時,我方的接口按照某個協議編寫,并且保持固定不變。然后,在與真正對方接口時,在前段設計一個適配器類,一旦對方協議發生變更,我可以換個適配器,將新協議轉換成原協議,問題就解決了。適配器模式應當包含一個接口和它的實現類。接口應當包含一個本系統要調用的方法,而它的實現類分別是與 A 系統接口的適配器、與 B 系統接口的適配器...
?
?
?
我曾經在一個項目中需要與另一個系統接口,起初那個系統通過一個數據集的方式為我提供數據,我寫了一個接收數據集的適配器;后來改為用一個 XML 數據流的形式,我又寫了一個接收 XML 的適配器。雖然為我提供數據的方式不同,但是經過適配器轉換后,輸出的數據是一樣的。通過在 spring 中的配置,我可以靈活地切換到底是使用哪個適配器。
?
d. 模板模式
32 個經典模式中的模板模式,對開發者的代碼規劃能力提出了更高的要求,它要求開發者對自己開發的所有代碼有一個相互聯系和從中抽象的能力,從各個不同的模塊和各個不同的功能中,抽象出其過程比較一致的通用流程,最終形成模板。譬如說,讀取 XML 并形成工廠,是許多模塊常常要使用的功能。它們雖然有各自的不同,但是總體流程都是一樣的:讀取 XML 文件、解析 XML 數據流、形成工廠。正因為有這樣的特征,它們可以使用共同的模板,那么,什么是模板模式呢?
模板模式( Template Model )通常有一個抽象類。在這個抽象類中,通常有一個主函數,按照一定地順序去調用其它函數。而其它函數往往是某這個連續過程中的各個步驟,如以上實例中的讀取 XML 文件、解析 XML 數據流、形成工廠等步驟。由于這是一個抽象類,這些步驟函數可以是抽象函數。抽象類僅僅定義了整個過程的執行順序,以及一些可以通用的步驟(如讀取 XML 文件和解析 XML 數據流),而另一些比較個性的步驟,則由它的繼承類自己去完成(如上例中的“形成工廠”,由于各個工廠各不一樣,因此由各自的繼承類自己去決定它的工廠是怎樣形成的)。
?
?
?
各個繼承類可以根據自己的需要,通過重載重新定義各個步驟函數。但是,模板模式要求不能重載主函數,因此正規的模板模式其主函數應當是 final (雖然我們常常不這么寫)。另外,模板模式還允許你定義的這個步驟中,有些步驟是可選步驟。對與可選步驟,我們通常稱為“鉤子( hood )”。它在編寫時,在抽象類中并不是一個抽象函數,但卻是一個什么都不寫的空函數。繼承類在編寫時,如果需要這個步驟則重載這個函數,否則就什么也不寫,進而在執行的時候也如同什么都沒有執行。
通過以上對模板模式的描述可以發現,模板模式可以大大地提高我們的代碼復用程度。
以上一些常用設計模式,都能使我們快速提高代碼質量。還是那句話,設計模式不是什么高深的東西,恰恰相反,它是初學者快速提高的捷徑。然而,如果說提高代碼復用是提高代碼質量的初階,使用設計模式也只能是提高代碼質量的中階。那么,什么是高階呢?我認為是那些分析設計理論,更具體地說,就是職責驅動設計和領域驅動設計。
?
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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