一堂如何提高代碼質(zhì)量的培訓課
今天這堂培訓課講什么呢?我既不講Spring,也不講Hibernate,更不講Ext,我不講任何一個具體的技術(shù)。我們拋開任何具體的技術(shù),來談談如何提高代碼質(zhì)量。如何提高代碼質(zhì)量,相信不僅是在座所有人苦惱的事情,也是所有軟件項目苦惱的事情。如何提高代碼質(zhì)量呢,我認為我們首先要理解什么是高質(zhì)量的代碼。
高質(zhì)量代碼的三要素
我們評價高質(zhì)量代碼有三要素:可讀性、可維護性、可變更性。我們的代碼要一個都不能少地達到了這三要素的要求才能算高質(zhì)量的代碼。
1. 可讀性強
一提到可讀性似乎有一些老生常談的味道,但令人沮喪的是,雖然大家一而再,再而三地強調(diào)可讀性,但我們的代碼在可讀性方面依然做得非常糟糕。由于工作的需要,我常常需要去閱讀他人的代碼,維護他人設計的模塊。每當我看到大段大段、密密麻麻的代碼,而且還沒有任何的注釋時常常感慨不已,深深體會到了這項工作的重要。由于分工的需要,我們寫的代碼難免需要別人去閱讀和維護的。而對于許多程序員來說,他們很少去閱讀和維護別人的代碼。正因為如此,他們很少關(guān)注代碼的可讀性,也對如何提高代碼的可讀性缺乏切身體會。有時即使為代碼編寫了注釋,也常常是注釋語言晦澀難懂形同天書,令閱讀者反復斟酌依然不明其意。針對以上問題,我給大家以下建議:
1)不要編寫大段的代碼
如果你有閱讀他人代碼的經(jīng)驗,當你看到別人寫的大段大段的代碼,而且還不怎么帶注釋,你是怎樣的感覺,是不是“嗡”地一聲頭大。各種各樣的功能糾纏在一個方法中,各種變量來回調(diào)用,相信任何人多不會認為它是高質(zhì)量的代碼,但卻頻繁地出現(xiàn)在我們編寫的程序了。如果現(xiàn)在你再回顧自己寫過的代碼,你會發(fā)現(xiàn),稍微編寫一個復雜的功能,幾百行的代碼就出去了。一些比較好的辦法就是分段。將大段的代碼經(jīng)過整理,分為功能相對獨立的一段又一段,并且在每段的前端編寫一段注釋。這樣的編寫,比前面那些雜亂無章的大段代碼確實進步了不少,但它們在功能獨立性、可復用性、可維護性方面依然不盡人意。從另一個比較專業(yè)的評價標準來說,它沒有實現(xiàn)低耦合、高內(nèi)聚。我給大家的建議是,將這些相對獨立的段落另外封裝成一個又一個的函數(shù)。
許多大師在自己的經(jīng)典書籍中,都鼓勵我們在編寫代碼的過程中應當養(yǎng)成不斷重構(gòu)的習慣。我們在編寫代碼的過程中常常要編寫一些復雜的功能,起初是寫在一個類的一個函數(shù)中。隨著功能的逐漸展開,我們開始對復雜功能進行歸納整理,整理出了一個又一個的獨立功能。這些獨立功能有它與其它功能相互交流的輸入輸出數(shù)據(jù)。當我們分析到此處時,我們會非常自然地要將這些功能從原函數(shù)中分離出來,形成一個又一個獨立的函數(shù),供原函數(shù)調(diào)用。在編寫這些函數(shù)時,我們應當仔細思考一下,為它們?nèi)∫粋€釋義名稱,并為它們編寫注釋(后面還將詳細討論這個問題)。另一個需要思考的問題是,這些函數(shù)應當放到什么地方。這些函數(shù)可能放在原類中,也可能放到其它相應職責的類中,其遵循的原則應當是“職責驅(qū)動設計”(后面也將詳細描述)。
下面是我編寫的一個從XML文件中讀取數(shù)據(jù),將其生成工廠的一個類。這個類最主要的一段程序就是初始化工廠,該功能歸納起來就是三部分功能:用各種方式嘗試讀取文件、以DOM的方式解析XML數(shù)據(jù)流、生成工廠。而這些功能被我歸納整理后封裝在一個不同的函數(shù)中,并且為其取了釋義名稱和編寫了注釋:
- /**
- * 初始化工廠。根據(jù)路徑讀取XML文件,將XML文件中的數(shù)據(jù)裝載到工廠中
- * @param path XML的路徑
- */
- public void initFactory(String path){
- if(findOnlyOneFileByClassPath(path)){return;}
- if(findResourcesByUrl(path)){return;}
- if(findResourcesByFile(path)){return;}
- this.paths = new String[]{path};
- }
- /**
- * 初始化工廠。根據(jù)路徑列表依次讀取XML文件,將XML文件中的數(shù)據(jù)裝載到工廠中
- * @param paths 路徑列表
- */
- public void initFactory(String[] paths){
- for(int i=0; i<paths.length; i++){
- initFactory(paths[i]);
- }
- this.paths = paths;
- }
- /**
- * 重新初始化工廠,初始化所需的參數(shù),為上一次初始化工廠所用的參數(shù)。
- */
- public void reloadFactory(){
- initFactory(this.paths);
- }
- /**
- * 采用ClassLoader的方式試圖查找一個文件,并調(diào)用<code>readXmlStream()</code>進行解析
- * @param path XML文件的路徑
- * @return 是否成功
- */
- protected boolean findOnlyOneFileByClassPath(String path){
- boolean success = false;
- try {
- Resource resource = new ClassPathResource(path, this.getClass());
- resource.setFilter(this.getFilter());
- InputStream is = resource.getInputStream();
- if(is==null){return false;}
- readXmlStream(is);
- success = true;
- } catch (SAXException e) {
- log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
- } catch (IOException e) {
- log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
- } catch (ParserConfigurationException e) {
- log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
- }
- return success;
- }
- /**
- * 采用URL的方式試圖查找一個目錄中的所有XML文件,并調(diào)用<code>readXmlStream()</code>進行解析
- * @param path XML文件的路徑
- * @return 是否成功
- */
- protected boolean findResourcesByUrl(String path){
- boolean success = false;
- try {
- ResourcePath resourcePath = new PathMatchResource(path, this.getClass());
- resourcePath.setFilter(this.getFilter());
- Resource[] loaders = resourcePath.getResources();
- for(int i=0; i<loaders.length; i++){
- InputStream is = loaders[i].getInputStream();
- if(is!=null){
- readXmlStream(is);
- success = true;
- }
- }
- } catch (SAXException e) {
- log.debug("Error when findResourcesByUrl:"+path,e);
- } catch (IOException e) {
- log.debug("Error when findResourcesByUrl:"+path,e);
- } catch (ParserConfigurationException e) {
- log.debug("Error when findResourcesByUrl:"+path,e);
- }
- return success;
- }
- /**
- * 用File的方式試圖查找文件,并調(diào)用<code>readXmlStream()</code>解析
- * @param path XML文件的路徑
- * @return 是否成功
- */
- protected boolean findResourcesByFile(String path){
- boolean success = false;
- FileResource loader = new FileResource(new File(path));
- loader.setFilter(this.getFilter());
- try {
- Resource[] loaders = loader.getResources();
- if(loaders==null){return false;}
- for(int i=0; i<loaders.length; i++){
- InputStream is = loaders[i].getInputStream();
- if(is!=null){
- readXmlStream(is);
- success = true;
- }
- }
- } catch (IOException e) {
- log.debug("Error when findResourcesByFile:"+path,e);
- } catch (SAXException e) {
- log.debug("Error when findResourcesByFile:"+path,e);
- } catch (ParserConfigurationException e) {
- log.debug("Error when findResourcesByFile:"+path,e);
- }
- return success;
- }
- /**
- * 讀取并解析一個XML的文件輸入流,以Element的形式獲取XML的根,
- * 然后調(diào)用<code>buildFactory(Element)</code>構(gòu)建工廠
- * @param inputStream 文件輸入流
- * @throws SAXException
- * @throws IOException
- * @throws ParserConfigurationException
- */
- protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{
- if(inputStream==null){
- throw new ParserConfigurationException("Cann't parse source because of InputStream is null!");
- }
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setValidating(this.isValidating());
- factory.setNamespaceAware(this.isNamespaceAware());
- DocumentBuilder build = factory.newDocumentBuilder();
- Document doc = build.parse(new InputSource(inputStream));
- Element root = doc.getDocumentElement();
- buildFactory(root);
- }
- /**
- * 用從一個XML的文件中讀取的數(shù)據(jù)構(gòu)建工廠
- * @param root 從一個XML的文件中讀取的數(shù)據(jù)的根
- */
- protected abstract void buildFactory(Element root);
在編寫代碼的過程中,通常有兩種不同的方式。一種是從下往上編寫,也就是按照順序,每分出去一個函數(shù),都要將這個函數(shù)編寫完,才回到主程序,繼續(xù)往下編寫。而一些更有經(jīng)驗的程序員會采用另外一種從上往下的編寫方式。當他們在編寫程序的時候,每個被分出去的程序,可以暫時只寫一個空程序而不去具體實現(xiàn)功能。當主程序完成以后,再一個個實現(xiàn)它的所有子程序。采用這樣的編寫方式,可以使復雜程序有更好的規(guī)劃,避免只見樹木不見森林的弊病。
有多少代碼就算大段代碼,每個人有自己的理解。我編寫代碼,每當達到15~20行的時候,我就開始考慮是否需要重構(gòu)代碼。同理,一個類也不應當有太多的函數(shù),當函數(shù)達到一定程度的時候就應該考慮分為多個類了;一個包也不應當有太多的類······
2)釋義名稱與注釋
我們在命名變量、函數(shù)、屬性、類以及包的時候,應當仔細想想,使名稱更加符合相應的功能。我們常常在說,設計一個系統(tǒng)時應當有一個或多個系統(tǒng)分析師對整個系統(tǒng)的包、類以及相關(guān)的函數(shù)和屬性進行規(guī)劃,但在通常的項目中這都非常難于做到。對它們的命名更多的還是程序員來完成。但是,在一個項目開始的時候,應當對項目的命名出臺一個規(guī)范。譬如,在我的項目中規(guī)定,新增記錄用new或add開頭,更新記錄用edit或mod開頭,刪除用del開頭,查詢用 find或query開頭。使用最亂的就是get,因此我規(guī)定,get開頭的函數(shù)僅僅用于獲取類屬性。
注釋是每個項目組都在不斷強調(diào)的,可是依然有許多的代碼沒有任何的注釋。為什么呢?因為每個項目在開發(fā)過程中往往時間都是非常緊的。在緊張的代碼開發(fā)過程中,注釋往往就漸漸地被忽略了。利用開發(fā)工具的代碼編寫模板也許可以解決這個問題。
用我們常用的MyEclipse為例,在菜單“window>>Preferences>>Java>>Code Style>>Code Templates>>Comments”中,可以簡單的修改一下。
“Files”代表的是我們每新建一個文件(可能是類也可能是接口)時編寫的注釋,我通常設定為:
- /*
- * created on ${date}
- */
“Types”代表的是我們新建的接口或類前的注釋,我通常設定為:
- /**
- *
- * @author ${user}
- */
第一行為一個空行,是用于你寫該類的注釋。如果你采用“職責驅(qū)動設計”,這里首先應當描述的是該類的職責。如果需要,你可以寫該類一些重要的方法及其用法、該類的屬性及其中文含義等。
${user}代表的是你在windows中登陸的用戶名。如果這個用戶名不是你的名稱,你可以直接寫死為你自己的名稱。
其它我通常都保持為默認值。通過以上設定,你在創(chuàng)建類或接口的時候,系統(tǒng)將自動為你編寫好注釋,然后你可以在這個基礎上進行修改,大大提高注釋編寫的效率。
同時,如果你在代碼中新增了一個函數(shù)時,通過Alt+Shift+J快捷鍵,可以按照模板快速添加注釋。
在編寫代碼時如果你編寫的是一個接口或抽象類,我還建議你在@author后面增加@see注釋,將該接口或抽象類的所有實現(xiàn)類列出來,因為閱讀者在閱讀的時候,尋找接口或抽象類的實現(xiàn)類比較困難。
- /**
- * 抽象的單表數(shù)組查詢實現(xiàn)類,僅用于單表查詢
- * @author 范鋼
- * @see com.htxx.support.query.DefaultArrayQuery
- * @see com.htxx.support.query.DwrQuery
- */
- public abstract class ArrayQuery implements ISingleQuery {
- ...
#p#
2. 可維護性
軟件的可維護性有幾層意思,首先的意思就是能夠適應軟件在部署和使用中的各種情況。從這個角度上來說,它對我們的軟件提出的要求就是不能將代碼寫死。
1)代碼不能寫死
我曾經(jīng)見我的同事將系統(tǒng)要讀取的一個日志文件指定在C盤的一個固定目錄下,如果系統(tǒng)部署時沒有這個目錄以及這個文件就會出錯。如果他將這個決定路徑下的目錄改為相對路徑,或者通過一個屬性文件可以修改,代碼豈不就寫活了。一般來說,我在設計中需要使用日志文件、屬性文件、配置文件,通常都是以下幾個方式:將文件放到與類相同的目錄,使用ClassLoader.getResource()來讀取;將文件放到classpath目錄下,用File的相對路徑來讀取;使用web.xml或另一個屬性文件來制定讀取路徑。
我也曾見另一家公司的軟件要求,在部署的時候必須在C:/bea目錄下,如果換成其它目錄則不能正常運行。這樣的設定常常為軟件部署時帶來許多的麻煩。如果服務器在該目錄下已經(jīng)沒有多余空間,或者已經(jīng)有其它軟件,將是很撓頭的事情。
2)預測可能發(fā)生的變化
除此之外,在設計的時候,如果將一些關(guān)鍵參數(shù)放到配置文件中,可以為軟件部署和使用帶來更多的靈活性。要做到這一點,要求我們在軟件設計時,應當有更多的意識,考慮到軟件應用中可能發(fā)生的變化。比如,有一次我在設計財務軟件的時候,考慮到一些單據(jù)在制作時的前置條件,在不同企業(yè)使用的時候,可能要求不一樣,有些企業(yè)可能要求嚴格些而有些要求松散些??紤]到這種可能的變化,我將前置條件設計為可配置的,就可能方便部署人員在實際部署中進行靈活變化。然而這樣的配置,必要的注釋說明是非常必要的。
軟件可維護性的另一層意思就是軟件的設計便于日后的變更。這一層意思與軟件的可變更性是重合的。所有的軟件設計理論的發(fā)展,都是從軟件的可變更性這一要求逐漸展開的,它成為了軟件設計理論的核心。
#p#
3. 可變更性
前面我提到了,軟件的變更性是所有軟件理論的核心,那么什么是軟件的可變更性呢?按照現(xiàn)在的軟件理論,客戶對軟件的需求時時刻刻在發(fā)生著變化。當軟件設計好以后,為應對客戶需求的變更而進行的代碼修改,其所需要付出的代價,就是軟件設計的可變更性。由于軟件合理的設計,修改所付出的代價越小,則軟件的可變更性越好,即代碼設計的質(zhì)量越高。一種非常理想的狀態(tài)是,無論客戶需求怎樣變化,軟件只需進行適當?shù)男薷木湍軌蜻m應。但這之所以稱之為理想狀態(tài),因為客戶需求變化是有大有小的。如果客戶需求變化非常大,即使再好的設計也無法應付,甚至重新開發(fā)。然而,客戶需求的適當變化,一個合理的設計可以使得變更代價最小化,延續(xù)我們設計的軟件的生命力。
1)通過提高代碼復用提高可維護性
我曾經(jīng)遇到過這樣一件事,我要維護的一個系統(tǒng)因為應用范圍的擴大,它對機關(guān)級次的計算方式需要改變一種策略。如果這個項目統(tǒng)一采用一段公用方法來計算機關(guān)級次,這樣一個修改實在太簡單了,就是修改這個公用方法即可。但是,事實卻不一樣,對機關(guān)級次計算的代碼遍布整個項目,甚至有些還寫入到了那些復雜的SQL語句中。在這樣一種情況下,這樣一個需求的修改無異于需要遍歷這個項目代碼。這樣一個實例顯示了一個項目代碼復用的重要,然而不幸的是,代碼無法很好復用的情況遍布我們所有的項目。代碼復用的道理十分簡單,但要具體運作起來非常復雜,它除了需要很好的代碼規(guī)劃,還需要持續(xù)地代碼重構(gòu)。
對整個系統(tǒng)的整體分析與合理規(guī)劃可以根本地保證代碼復用。系統(tǒng)分析師通過用例模型、領域模型、分析模型的一步一步分析,最后通過正向工程,生成系統(tǒng)需要設計的各種類及其各自的屬性和方法。采用這種方法,功能被合理地劃分到這個類中,可以很好地保證代碼復用。
采用以上方法雖然好,但技術(shù)難度較高,需要有高深的系統(tǒng)分析師,并不是所有項目都能普遍采用的,特別是時間比較緊張的項目。通過開發(fā)人員在設計過程中的重構(gòu),也許更加實用。當某個開發(fā)人員在開發(fā)一段代碼時,發(fā)現(xiàn)該功能與前面已經(jīng)開發(fā)功能相同,或者部分相同。這時,這個開發(fā)人員可以對前面已經(jīng)開發(fā)的功能進行重構(gòu),將可以通用的代碼提取出來,進行相應的改造,使其具有一定的通用性,便于各個地方可以使用。
一些比較成功的項目組會指定一個專門管理通用代碼的人,負責收集和整理項目組中各個成員編寫的、可以通用的代碼。這個負責人同時也應當具有一定的代碼編寫功力,因為將專用代碼提升為通用代碼,或者以前使用該通用代碼的某個功能,由于業(yè)務變更,而對這個通用代碼的變更要求,都對這個負責人提出了很高的能力要求。
雖然后一種方式非常實用,但是它有些亡羊補牢的味道,不能從整體上對項目代碼進行有效規(guī)劃。正因為兩種方法各有利弊,因此在項目中應當配合使用。
2)利用設計模式提高可變更性
對于初學者,軟件設計理論常常感覺晦澀難懂。一個快速提高軟件質(zhì)量的捷徑就是利用設計模式。這里說的設計模式,不僅僅指經(jīng)典的32個模式,是一切前人總結(jié)的,我們可以利用的、更加廣泛的設計模式。
a. if…else… |
這個我也不知道叫什么名字,最早是哪位大師總結(jié)的,它出現(xiàn)在Larman的《UML與模式應用》,也出現(xiàn)在出現(xiàn)在Mardin的《敏捷軟件開發(fā)》。它是這樣描述的:當你發(fā)現(xiàn)你必須要設計這樣的代碼:“if…elseif…elseif…else…”時,你應當想到你的代碼應當重構(gòu)一下了。我們先看看這樣的代碼有怎樣的特點。
- if(var.equals("A")){ doA(); }
- else if(var.equals("B")){ doB(); }
- else if(var.equals("C")){ doC(); }
- else{ doD(); }
這樣的代碼很常見,也非常平常,我們大家都寫過。但正是這樣平常才隱藏著我們永遠沒有注意的問題。問題就在于,如果某一天這個選項不再僅僅是A、 B、C,而是增加了新的選項,會怎樣呢?你也許會說,那沒有關(guān)系,我把代碼改改就行。然而事實上并非如此,在大型軟件研發(fā)與維護中有一個原則,每次的變更盡量不要去修改原有的代碼。如果我們重構(gòu)一下,能保證不修改原有代碼,僅僅增加新的代碼就能應付選項的增加,這就增加了這段代碼的可維護性和可變更性,提高了代碼質(zhì)量。那么,我們應當如何去做呢?
經(jīng)過深入分析你會發(fā)現(xiàn),這里存在一個對應關(guān)系,即A對應doA(),B對應doB()...如果將doA()、doB()、doC()...與原有代碼解耦,問題就解決了。如何解耦呢?設計一個接口X以及它的實現(xiàn)A、B、C...每個類都包含一個方法doX(),并且將doA()的代碼放到 A.doX()中,將doB()的代碼放到B.doX()中...經(jīng)過以上的重構(gòu),代碼還是這些代碼,效果卻完全不一樣了。我們只需要這樣寫:
- X x = factory.getBean(var); x.doX();
這樣就可以實現(xiàn)以上的功能了。我們看到這里有一個工廠,放著所有的A、B、C...并且與它們的key對應起來,并且寫在配置文件中。如果出現(xiàn)新的選項時,通過修改配置文件就可以無限制的增加下去。
這個模式雖然有效提高了代碼質(zhì)量,但是不能濫用,并非只要出現(xiàn)if…else…就需要使用。由于它使用了工廠,一定程度上增加了代碼復雜度,因此僅僅在選項較多,并且增加選項的可能性很大的情況下才可以使用。另外,要使用這個模式,繼承我在附件中提供的抽象類 XmlBuildFactoryFacade就可以快速建立一個工廠。如果你的項目放在spring或其它可配置框架中,也可以快速建立工廠。設計一個 Map靜態(tài)屬性并使其V為這些A、B、C...這個工廠就建立起來了。
b. 策略模式
也許你看過策略模式(strategy model)的相關(guān)資料但沒有留下太多的印象。一個簡單的例子可以讓你快速理解它。如果一個員工系統(tǒng)中,員工被分為臨時工和正式工并且在不同的地方相應的行為不一樣。在設計它們的時候,你肯定設計一個抽象的員工類,并且設計兩個繼承類:臨時工和正式工。這樣,通過下溯類型,可以在不同的地方表現(xiàn)出臨時工和正式工的各自行為。在另一個系統(tǒng)中,員工被分為了銷售人員、技術(shù)人員、管理人員并且也在不同的地方相應的行為不一樣。同樣,我們在設計時也是設計一個抽象的員工類,并且設計數(shù)個繼承類:銷售人員、技術(shù)人員、管理人員?,F(xiàn)在,我們要把這兩個系統(tǒng)合并起來,也就是說,在新的系統(tǒng)中,員工既被分為臨時工和正式工,又被分為了銷售人員、技術(shù)人員、管理人員,這時候如何設計。如果我們還是使用以往的設計,我們將不得不設計很多繼承類:銷售臨時工、銷售正式工、技術(shù)臨時工、技術(shù)正式工。。。如此的設計,在隨著劃分的類型,以及每種類型的選項的增多,呈笛卡爾增長。通過以上一個系統(tǒng)的設計,我們不得不發(fā)現(xiàn),我們以往學習的關(guān)于繼承的設計遇到了挑戰(zhàn)。
解決繼承出現(xiàn)的問題,有一個最好的辦法,就是采用策略模式。在這個應用中,員工之所以要分為臨時工和正式工,無非是因為它們的一些行為不一樣,比如,發(fā)工資時的計算方式不同。如果我們在設計時不將員工類分為臨時工類和正式工類,而僅僅只有員工類,只是在類中增加“工資發(fā)放策略”。當我們創(chuàng)建員工對象時,根據(jù)員工的類型,將“工資發(fā)放策略”設定為“臨時工策略”或“正式工策略”,在計算工資時,只需要調(diào)用策略類中的“計算工資”方法,其行為的表現(xiàn),也設計臨時工類和正式工類是一樣的。同樣的設計可以放到銷售人員策略、技術(shù)人員策略、管理人員策略中。一個通常的設計是,我們將某一個影響更大的、或者選項更少的屬性設計成繼承類,而將其它屬性設計成策略類,就可以很好的解決以上問題。
使用策略模式,你同樣把代碼寫活了,因為你可以無限制地增加策略。但是,使用策略模式你同樣需要設計一個工廠——策略工廠。以上實例中,你需要設計一個發(fā)放工資策略工廠,并且在工廠中將“臨時工”與“臨時工策略”對應起來,將“正式工”與“正式工策略”對應起來。
c. 適配器模式
我的筆記本是港貨,它的插頭與我們常用的插座不一樣,所有我出差的時候我必須帶一個適配器,才能使用不同地方的插座。這是一個對適配器模式最經(jīng)典的描述。當我們設計的系統(tǒng)要與其它系統(tǒng)交互,或者我們設計的模塊要與其它模塊交互時,這種交互可能是調(diào)用一個接口,或者交換一段數(shù)據(jù),接受方常常因發(fā)送方對協(xié)議的變更而頻繁變更。這種變更,可能是接受方來源的變更,比如原來是A系統(tǒng),現(xiàn)在變成B系統(tǒng)了;也可能是接受方自身的代碼變更,如原來的接口現(xiàn)在增加了一個參數(shù)。由于發(fā)送方的變更常常導致接受方代碼的不穩(wěn)定,即頻繁跟著修改,為接受方的維護帶來困難。
遇到這樣的問題,一個有經(jīng)驗的程序員馬上想到的就是采用適配器模式。在設計時,我方的接口按照某個協(xié)議編寫,并且保持固定不變。然后,在與真正對方接口時,在前段設計一個適配器類,一旦對方協(xié)議發(fā)生變更,我可以換個適配器,將新協(xié)議轉(zhuǎn)換成原協(xié)議,問題就解決了。適配器模式應當包含一個接口和它的實現(xiàn)類。接口應當包含一個本系統(tǒng)要調(diào)用的方法,而它的實現(xiàn)類分別是與A系統(tǒng)接口的適配器、與B系統(tǒng)接口的適配器...
我曾經(jīng)在一個項目中需要與另一個系統(tǒng)接口,起初那個系統(tǒng)通過一個數(shù)據(jù)集的方式為我提供數(shù)據(jù),我寫了一個接收數(shù)據(jù)集的適配器;后來改為用一個XML數(shù)據(jù)流的形式,我又寫了一個接收XML的適配器。雖然為我提供數(shù)據(jù)的方式不同,但是經(jīng)過適配器轉(zhuǎn)換后,輸出的數(shù)據(jù)是一樣的。通過在spring中的配置,我可以靈活地切換到底是使用哪個適配器。
d. 模板模式
32個經(jīng)典模式中的模板模式,對開發(fā)者的代碼規(guī)劃能力提出了更高的要求,它要求開發(fā)者對自己開發(fā)的所有代碼有一個相互聯(lián)系和從中抽象的能力,從各個不同的模塊和各個不同的功能中,抽象出其過程比較一致的通用流程,最終形成模板。譬如說,讀取XML并形成工廠,是許多模塊常常要使用的功能。它們雖然有各自的不同,但是總體流程都是一樣的:讀取XML文件、解析XML數(shù)據(jù)流、形成工廠。正因為有這樣的特征,它們可以使用共同的模板,那么,什么是模板模式呢?
模板模式(Template Model)通常有一個抽象類。在這個抽象類中,通常有一個主函數(shù),按照一定地順序去調(diào)用其它函數(shù)。而其它函數(shù)往往是某這個連續(xù)過程中的各個步驟,如以上實例中的讀取XML文件、解析XML數(shù)據(jù)流、形成工廠等步驟。由于這是一個抽象類,這些步驟函數(shù)可以是抽象函數(shù)。抽象類僅僅定義了整個過程的執(zhí)行順序,以及一些可以通用的步驟(如讀取XML文件和解析XML數(shù)據(jù)流),而另一些比較個性的步驟,則由它的繼承類自己去完成(如上例中的“形成工廠”,由于各個工廠各不一樣,因此由各自的繼承類自己去決定它的工廠是怎樣形成的)。
各個繼承類可以根據(jù)自己的需要,通過重載重新定義各個步驟函數(shù)。但是,模板模式要求不能重載主函數(shù),因此正規(guī)的模板模式其主函數(shù)應當是 final(雖然我們常常不這么寫)。另外,模板模式還允許你定義的這個步驟中,有些步驟是可選步驟。對與可選步驟,我們通常稱為“鉤子(hood)”。它在編寫時,在抽象類中并不是一個抽象函數(shù),但卻是一個什么都不寫的空函數(shù)。繼承類在編寫時,如果需要這個步驟則重載這個函數(shù),否則就什么也不寫,進而在執(zhí)行的時候也如同什么都沒有執(zhí)行。
通過以上對模板模式的描述可以發(fā)現(xiàn),模板模式可以大大地提高我們的代碼復用程度。
以上一些常用設計模式,都能使我們快速提高代碼質(zhì)量。還是那句話,設計模式不是什么高深的東西,恰恰相反,它是初學者快速提高的捷徑。然而,如果說提高代碼復用是提高代碼質(zhì)量的初階,使用設計模式也只能是提高代碼質(zhì)量的中階。那么,什么是高階呢?我認為是那些分析設計理論,更具體地說,就是職責驅(qū)動設計和領域驅(qū)動設計。
3)職責驅(qū)動設計和領域驅(qū)動設計
前面我提到,當我們嘗試寫一些復雜功能的時候,我們把功能分解成一個個相對獨立的函數(shù)。但是,應當將這些函數(shù)分配到哪個類中呢?也就是系統(tǒng)中的所有類都應當擁有哪些函數(shù)呢?或者說應當表現(xiàn)出哪些行為呢?答案就在這里:以職責為中心,根據(jù)職責分配行為。我們在分析系統(tǒng)時,首先是根據(jù)客戶需求進行用例分析,然后根據(jù)用例繪制領域模式和分析模型,整個系統(tǒng)最主要的類就形成了。通過以上分析形成的類,往往和現(xiàn)實世界的對象是對應的。正因為如此,軟件世界的這些類也具有了與現(xiàn)實世界的對象相對應的職責,以及在這些職責范圍內(nèi)的行為。
職責驅(qū)動設計(Responsibility Drive Design,RDD)是Craig Larman在他的經(jīng)典著作《UML和模式應用》中提出的。職責驅(qū)動設計的核心思想,就是我們在對一個系統(tǒng)進行分析設計的時候,應當以職責為中心,根據(jù)職責分配行為。這種思想首先要求我們設計的所有軟件世界的對象,應當與現(xiàn)實世界盡量保持一致,他稱之為“低表示差異”。有了低表示差異,一方面提高了代碼的可讀性,另一方面,當業(yè)務發(fā)生變更的時候,也可以根據(jù)實際情況快速應對變更。
Craig Larman在提出職責驅(qū)動設計理論的同時,還提出了GRASP設計模式,來豐富這個理論。在GRASP設計模式中,我認為,低耦合、高內(nèi)聚、信息專家模式最有用。
繼Craig Larman提出的職責驅(qū)動設計數(shù)年之后,另一位大師提出了領域驅(qū)動設計。領域驅(qū)動設計(Domain Drive Design,DDD)是Eric Evans在他的同名著作《領域驅(qū)動設計》中提出的。在之前的設計理論中,領域模型是從用例模型到分析模型之間的一種中間模型,也就是從需求分析到軟件開發(fā)之間的一種中間模型。這么一個中間模型,既不是需求階段的重要產(chǎn)物,在開發(fā)階段也不以它作為標準進行開發(fā),僅僅是作為參考,甚至給人感覺有一些多余。但是,Evans在領域驅(qū)動設計中,將它放到了一個無比重要的位置。按照領域驅(qū)動設計的理論,在需求分析階段,需求分析人員使用領域模型與客戶進行溝通;在設計開發(fā)階段,開發(fā)人員使用領域模型指導設計開發(fā);在運行維護和二次開發(fā)階段,維護和二次開發(fā)人員使用領域模型理解和熟悉系統(tǒng),并指導他們進行維護和二次開發(fā)。總之,在整個軟件開發(fā)的生命周期中,領域模型都成為了最核心的內(nèi)容。
領域驅(qū)動設計繼承了職責驅(qū)動設計。在領域驅(qū)動設計中強調(diào)的,依然是低表示差異,以及職責的分配。但是,如何做到低表示差異呢?如何完成職責分配呢?領域驅(qū)動設計給了我們完美的答案,那就是建立領域模型。領域驅(qū)動設計改變了我們的設計方式。在需求分析階段,用例模型已不再是這個階段的核心,而是建立領域模型。在開發(fā)和二次開發(fā)階段,開發(fā)人員也不再是一埋頭地猛扎進程序堆里開始編程,而是首先細致地進行領域模型分析。領域驅(qū)動設計強調(diào)持續(xù)精化,使領域模型不再是一旦完成分析就扔在一邊不再理會的圖紙,而是在不斷理解業(yè)務的基礎上不斷修改和精化領域模型,進而驅(qū)動我們代碼的精化。領域驅(qū)動設計強調(diào)的不再是一次軟件開發(fā)過程中我們要做的工作,它看得更加長遠,它強調(diào)的是一套軟件在相當長一段時間內(nèi)持續(xù)升級的過程中我們應當做的工作。我認為,領域驅(qū)動設計是提高代碼質(zhì)量的最高等級。當時,使用領域驅(qū)動設計進行軟件開發(fā)是一場相當巨大的改革,它顛覆了我們過去的所有開發(fā)模式,我們必須腳踏實地地一步一步去實踐和改變。
#p#
職責驅(qū)動設計
隨著軟件業(yè)的不斷發(fā)展,隨著軟件需求的不斷擴大,軟件所管理的范圍也在不斷拓寬。過去一個軟件僅僅管理一臺電腦的一個小小的功能,而現(xiàn)在被擴展到了一個企業(yè)、一個行業(yè)、一個產(chǎn)業(yè)鏈。過去我們開發(fā)一套軟件,只有少量的二次開發(fā),當它使用到一定時候我們就拋棄掉重新又開發(fā)一套?,F(xiàn)在,隨著用戶對軟件依賴程度的不斷加大,我們很難說拋棄一套軟件重新開發(fā)了,更多的是在一套軟件中持續(xù)改進,使這套軟件的生命周期持續(xù)數(shù)年以及數(shù)個版本。正是因為軟件業(yè)面臨著如此巨大的壓力,我們的代碼質(zhì)量,我們開發(fā)的軟件擁有的可變更性和持續(xù)改進的能力,成為軟件制勝的關(guān)鍵因素,令我們不能不反思。
代碼質(zhì)量評價的關(guān)鍵指標:低耦合,高內(nèi)聚
耦合就是對某元素與其它元素之間的連接、感知和依賴的量度。耦合包括:
1.元素B是元素A的屬性,或者元素A引用了元素B的實例(這包括元素A調(diào)用的某個方法,其參數(shù)中包含元素B)。
2.元素A調(diào)用了元素B的方法。
3.元素A直接或間接成為元素B的子類。
4.元素A是接口B的實現(xiàn)。
如果一個元素過于依賴其它元素,一旦它所依賴的元素不存在,或者發(fā)生變更,則該元素將不能再正常運行,或者不得不相應地進行變更。因此,耦合將大大影響代碼的通用性和可變更性。
內(nèi)聚,更為專業(yè)的說法叫功能內(nèi)聚,是對軟件系統(tǒng)中元素職責相關(guān)性和集中度的度量。如果元素具有高度相關(guān)的職責,除了這些職責內(nèi)的任務,沒有其它過多的工作,那么該元素就具有高內(nèi)聚性,反之則為低內(nèi)聚性。內(nèi)聚就像一個專橫的管理者,它只做自己職責范圍內(nèi)的事,而將其它與它相關(guān)的事情,分配給別人去做。
高質(zhì)量的代碼要求我們的代碼保持低耦合、高內(nèi)聚。但是,這個要求是如此的抽象與模糊,如何才能做到這些呢?軟件大師們告訴我們了許多方法,其中之一就是Craig Larman的職責驅(qū)動設計。
職責驅(qū)動設計(Responsibility Drive Design,RDD)是Craig Larman在他的經(jīng)典著作《UML和模式應用》中提出的。要理解職責驅(qū)動設計,我們首先要理解“低表示差異”。
低表示差異
我們開發(fā)的應用軟件實際上是對現(xiàn)實世界的模擬,因此,軟件世界與現(xiàn)實世界存在著必然的聯(lián)系。當我們在進行需求分析的時候,需求分析員實際上是從客戶那里在了解現(xiàn)實世界事物的規(guī)則、工作的流程。如果我們在軟件分析和設計的過程中,將軟件世界與現(xiàn)實世界緊密地聯(lián)系到一起,我們的軟件將更加本色地還原事物最本質(zhì)的規(guī)律。這樣的設計,就稱之為“低表示差異”。
采用“低表示差異”進行軟件設計,現(xiàn)實世界有什么事物,就映射為軟件世界的各種對象(類);現(xiàn)實世界的事物擁有什么樣的職責,在軟件世界里的對象就擁有什么樣的職責;在現(xiàn)實世界中的事物,因為它的職責而產(chǎn)生的行為,在軟件世界中就反映為對象所擁有的函數(shù)。
低表示差異,使分析設計者對軟件的分析和設計更加簡單,思路更加清晰;使代碼更加可讀,閱讀者更加易于理解;更重要的是,當需求發(fā)生變更,或者業(yè)務產(chǎn)生擴展時,設計者只需要遵循事物本來的面貌去思考和修改軟件,使軟件更加易于變更和擴展。
角色、職責、協(xié)作
理解了“低表示差異”,現(xiàn)在我們來看看我們應當如何運用職責驅(qū)動設計進行分析和設計。首先,我們通過與客戶的溝通和對業(yè)務需求的了解,從中提取出現(xiàn)實世界中的關(guān)鍵事物以及相互之間的關(guān)系。這個過程我們通常通過建立領域模型來完成。領域模型建立起來以后,通過諸如Rational Rose這樣的設計軟件的正向工程,生成了我們在軟件系統(tǒng)中最初始的軟件類。這些軟件類,由于每個都扮演著現(xiàn)實世界中的一個具體的角色,因而賦予了各自的職責。前面我已經(jīng)提到,如果你的系統(tǒng)采用職責驅(qū)動設計的思想進行設計開發(fā),作為一個好的習慣,你應當在每一個軟件類的注釋首行,清楚地描述該軟件類的職責。
當我們完成了系統(tǒng)中軟件類的制訂,分配好了各自的職責,我們就應該開始根據(jù)軟件需求,編寫各個軟件類的功能。在前面我給大家提出了一個建議,就是不要在一個函數(shù)中編寫大段的代碼。編寫大段的代碼,通常會降低代碼的內(nèi)聚度,因為這些代碼中將包含不是該軟件類應當完成的工作。作為一個有經(jīng)驗的開發(fā)人員,在編寫一個功能時,首先應當對功能進行分解。一段稍微復雜的功能,通常都可以被分解成一個個相對獨立的步驟。步驟與步驟之間存在著交互,那就是數(shù)據(jù)的輸入輸出。通過以上的分解,每一個步驟將形成一個獨立的函數(shù),并且使用一個可以表明這個步驟意圖的釋義函數(shù)名。接下來,我們應當考慮的,就是應當將這些函數(shù)交給誰。它們有可能交給原軟件類,也有可能交給其它軟件類,其分配的原則是什么呢?答案是否清楚,那就是職責。每個軟件類代表現(xiàn)實世界的一個事物,或者說一個角色。在現(xiàn)實世界中這個任務應當由誰來完成,那么在軟件世界中,這個函數(shù)就應當分配給相應的那個軟件類。
通過以上步驟的分解,一個功能就分配給了多個軟件類,相互協(xié)作地完成這個功能。這樣的分析和設計,其代碼一定是高內(nèi)聚的和高可讀性的。同時,當需求發(fā)生變更的時候,設計者通過對現(xiàn)實世界的理解,可以非常輕松地找到那個需要修改的軟件類,而不會影響其它類,因而也就變得易維護、易變更和低耦合了。
說了這么多,舉一個實例也許更能幫助理解。拿一個員工工資系統(tǒng)來說吧。當人力資源在發(fā)放一個月工資的時候,以及離職的員工肯定不能再發(fā)放工資了。在系統(tǒng)設計的期初,開發(fā)人員商量好,在員工信息中設定一個“離職標志”字段。編寫工資發(fā)放的開發(fā)人員通過查詢,將“離職標志”為false的員工查詢出來,并為他們計算和發(fā)放工資。但是,隨著這個系統(tǒng)的不斷使用,編寫員工管理的開發(fā)人員發(fā)現(xiàn),“離職標志”字段已經(jīng)不能滿足客戶的需求,因而將“離職標志”字段廢棄,并增加了一個“離職時間”字段來管理離職的員工。然而,編寫工資發(fā)放的開發(fā)人員并不知道這樣的變更,依然使用著“離職標志”字段。顯然,這樣的結(jié)果就是,軟件系統(tǒng)開始對離職員工發(fā)放工資了。仔細分析這個問題的原因,我們不難發(fā)現(xiàn),確認員工是否離職,并不是“發(fā)放工資”軟件類應當完成的工作,而應當是 “員工管理”軟件類應當完成的。如果將“獲取非離職員工”的任務交給“員工管理”軟件類,而“發(fā)放工資”軟件類僅僅只是去調(diào)用,那么離職功能由“離職標志”字段改為了“離職時間”字段,其實就與“發(fā)放工資”軟件類毫無關(guān)系。而作為“員工管理”的開發(fā)人員,一旦發(fā)生這樣的變更,他當然知道去修改自己相應的 “獲取非離職員工”函數(shù),這樣就不會發(fā)生以上問題。通過這樣一個實例,也許你能夠理解“職責驅(qū)動設計”的精要與作用了吧。
職責分配與信息專家
通過以上對職責驅(qū)動設計的講述,我們不難發(fā)現(xiàn),職責驅(qū)動設計的精要就是職責分配。但是,在紛繁復雜的軟件設計中,如何進行職責分配常常令我們迷惑。幸運的是,Larman大師清楚地認識到了這一點。在他的著作中,信息專家模式為我們提供了幫助。
信息專家模式(又稱為專家模式)告訴我們,在分析設計中,應當將職責分配給軟件系統(tǒng)中的這樣一個軟件類,它擁有實現(xiàn)這個職責所必須的信息。我們稱這個軟件類,叫“信息專家”。用更加簡短的話說,就是將職責分配給信息專家。
為什么我們要將職責分配給信息專家呢?我們用上面的例子來說明吧。當“發(fā)放工資”軟件類需要獲取非離職員工時,“員工管理”軟件類就是“獲取非離職員工”任務的信息專家,因為它掌握著所有員工的信息。假設我們不將“獲取非離職員工”的任務交給“員工管理”軟件類,而是另一個軟件類X,那么,為了獲取員工信息,軟件類X不得不訪問“員工管理”軟件類,從而使“發(fā)放工資”與X耦合,X又與“員工管理”耦合。這樣的設計,不如直接將“獲取非離職員工”的任務交給“員工管理”軟件類,使得“發(fā)放工資”僅僅與“員工管理”耦合,從而有效地降低了系統(tǒng)的整體耦合度。
總之,采用“職責驅(qū)動設計”的思路,為我們提高軟件開發(fā)質(zhì)量、可讀性、可維護性,以及保持軟件的持續(xù)發(fā)展,提供了一個廣闊的空間。