【Rules】高質量代碼規範

轉載自:https://www.cnblogs.com/guanghuiqq/archive/2012/11/06/2756876.htmlhtml

高質量代碼有三要素:可讀性、可維護性、可變動性前端

今天這堂培訓課講什麼呢?我既不講Spring,也不講Hibernate,更不講Ext,我不講任何一個具體的技術。咱們拋開任何具體的技術,來談談如何提升代碼質量。如何提升代碼質量,相信不只是在座全部人苦惱的事情,也是全部軟件項目苦惱的事情。如何提升代碼質量呢,我認爲咱們首先要理解什麼是高質量的代碼。程序員

  高質量代碼的三要素web

  咱們評價高質量代碼有三要素:可讀性、可維護性、可變動性。咱們的代碼要一個都不能少地達到了這三要素的要求才能算高質量的代碼。spring

  1. 可讀性強編程

  一提到可讀性彷佛有一些老生常談的味道,但使人沮喪的是,雖然你們一而再,再而三地強調可讀性,但咱們的代碼在可讀性方面依然作得很是糟糕。因爲工做的須要,我經常須要去閱讀他人的代碼,維護他人設計的模塊。每當我看到大段大段、密密麻麻的代碼,並且尚未任何的註釋時經常感慨不已,深深體會到了這項工做的重要。因爲分工的須要,咱們寫的代碼不免須要別人去閱讀和維護的。而對於許多程序員來講,他們不多去閱讀和維護別人的代碼。正由於如此,他們不多關注代碼的可讀性,也對如何提升代碼的可讀性缺少切身體會。有時即便爲代碼編寫了註釋,也經常是註釋語言晦澀難懂形同天書,令閱讀者反覆斟酌依然不明其意。針對以上問題,我給你們如下建議:windows

  1)不要編寫大段的代碼設計模式

  若是你有閱讀他人代碼的經驗,當你看到別人寫的大段大段的代碼,並且還不怎麼帶註釋,你是怎樣的感受,是否是「嗡」地一聲頭大。各類各樣的功能糾纏在一個方法中,各類變量來回調用,相信任何人多不會認爲它是高質量的代碼,但卻頻繁地出如今咱們編寫的程序了。若是如今你再回顧本身寫過的代碼,你會發現,稍微編寫一個複雜的功能,幾百行的代碼就出去了。一些比較好的辦法就是分段。將大段的代碼通過整理,分爲功能相對獨立的一段又一段,而且在每段的前端編寫一段註釋。這樣的編寫,比前面那些雜亂無章的大段代碼確實進步了很多,但它們在功能獨立性、可複用性、可維護性方面依然不盡人意。從另外一個比較專業的評價標準來講,它沒有實現低耦合、高內聚。我給你們的建議是,將這些相對獨立的段落另外封裝成一個又一個的函數。數組

  許多大師在本身的經典書籍中,都鼓勵咱們在編寫代碼的過程當中應當養成不斷重構的習慣。咱們在編寫代碼的過程當中經常要編寫一些複雜的功能,起初是寫在一個類的一個函數中。隨着功能的逐漸展開,咱們開始對複雜功能進行概括整理,整理出了一個又一個的獨立功能。這些獨立功能有它與其它功能相互交流的輸入輸出數據。當咱們分析到此處時,咱們會很是天然地要將這些功能從原函數中分離出來,造成一個又一個獨立的函數,供原函數調用。在編寫這些函數時,咱們應當仔細思考一下,爲它們取一個釋義名稱,併爲它們編寫註釋(後面還將詳細討論這個問題)。另外一個須要思考的問題是,這些函數應當放到什麼地方。這些函數可能放在原類中,也可能放到其它相應職責的類中,其遵循的原則應當是「職責驅動設計」(後面也將詳細描述)。服務器

  下面是我編寫的一個從XML文件中讀取數據,將其生成工廠的一個類。這個類最主要的一段程序就是初始化工廠,該功能概括起來就是三部分功能:用各類方式嘗試讀取文件、以DOM的方式解析XML數據流、生成工廠。而這些功能被我概括整理後封裝在一個不一樣的函數中,而且爲其取了釋義名稱和編寫了註釋:

  Java代碼

複製代碼

/**
  * 初始化工廠。根據路徑讀取XML文件,將XML文件中的數據裝載到工廠中
  * @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};
}
/**
* 初始化工廠。根據路徑列表依次讀取XML文件,將XML文件中的數據裝載到工廠中
* @param paths 路徑列表
*/
public void initFactory(String[] paths){
    for(int i=0; i<paths.length; i++){
        initFactory(paths[i]);
    }
    this.paths = paths;
}
/**
* 從新初始化工廠,初始化所需的參數,爲上一次初始化工廠所用的參數。
*/
public void reloadFactory(){
initFactory(this.paths);
}
/**
* 採用ClassLoader的方式試圖查找一個文件,並調用<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文件,並調用<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的方式試圖查找文件,並調用<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的根,
* 而後調用<code>buildFactory(Element)</code>構建工廠
* @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的文件中讀取的數據構建工廠
* @param root 從一個XML的文件中讀取的數據的根
*/
protected abstract void buildFactory(Element root);  

複製代碼

  在編寫代碼的過程當中,一般有兩種不一樣的方式。一種是從下往上編寫,也就是按照順序,每分出去一個函數,都要將這個函數編寫完,纔回到主程序,繼續往下編寫。而一些更有經驗的程序員會採用另一種從上往下的編寫方式。當他們在編寫程序的時候,每一個被分出去的程序,能夠暫時只寫一個空程序而不去具體實現功能。當主程序完成之後,再一個個實現它的全部子程序。採用這樣的編寫方式,可使複雜程序有更好的規劃,避免只見樹木不見森林的弊病。

  有多少代碼就算大段代碼,每一個人有本身的理解。我編寫代碼,每當達到15~20行的時候,我就開始考慮是否須要重構代碼。同理,一個類也不該當有太多的函數,當函數達到必定程度的時候就應該考慮分爲多個類了;一個包也不該當有太多的類······

  2)釋義名稱與註釋

  咱們在命名變量、函數、屬性、類以及包的時候,應當仔細想一想,使名稱更加符合相應的功能。咱們經常在說,設計一個系統時應當有一個或多個系統分析師對整個系統的包、類以及相關的函數和屬性進行規劃,但在一般的項目中這都很是難於作到。對它們的命名更多的仍是程序員來完成。可是,在一個項目開始的時候,應當對項目的命名出臺一個規範。譬如,在個人項目中規定,新增記錄用new或add開頭,更新記錄用edit或mod開頭,刪除用del開頭,查詢用find或query開頭。使用最亂的就是get,所以我規定,get開頭的函數僅僅用於獲取類屬性。

  註釋是每一個項目組都在不斷強調的,但是依然有許多的代碼沒有任何的註釋。爲何呢?由於每一個項目在開發過程當中每每時間都是很是緊的。在緊張的代碼開發過程當中,註釋每每就漸漸地被忽略了。利用開發工具的代碼編寫模板也許能夠解決這個問題。

  用咱們經常使用的MyEclipse爲例,在菜單「window>>Preferences>>Java>>Code Style>>Code Templates>>Comments」中,能夠簡單的修改一下。

  「Files」表明的是咱們每新建一個文件(多是類也多是接口)時編寫的註釋,我一般設定爲:

  Java代碼

/*
* created on ${date}
*/  

  「Types」表明的是咱們新建的接口或類前的註釋,我一般設定爲:

  Java代碼

/**
*
* @author ${user}
*/  

  第一行爲一個空行,是用於你寫該類的註釋。若是你採用「職責驅動設計」,這裏首先應當描述的是該類的職責。若是須要,你能夠寫該類一些重要的方法及其用法、該類的屬性及其中文含義等。

  ${user}表明的是你在windows中登錄的用戶名。若是這個用戶名不是你的名稱,你能夠直接寫死爲你本身的名稱。

  其它我一般都保持爲默認值。經過以上設定,你在建立類或接口的時候,系統將自動爲你編寫好註釋,而後你能夠在這個基礎上進行修改,大大提升註釋編寫的效率。

  同時,若是你在代碼中新增了一個函數時,經過Alt+Shift+J快捷鍵,能夠按照模板快速添加註釋。

  在編寫代碼時若是你編寫的是一個接口或抽象類,我還建議你在@author後面增長@see註釋,將該接口或抽象類的全部實現類列出來,由於閱讀者在閱讀的時候,尋找接口或抽象類的實現類比較困難。

  Java代碼

複製代碼

/**
* 抽象的單表數組查詢實現類,僅用於單表查詢
* @author 範鋼
* @see com.htxx.support.query.DefaultArrayQuery
* @see com.htxx.support.query.DwrQuery
*/
public abstract class ArrayQuery implements ISingleQuery {
...  

複製代碼

  2. 可維護性

  軟件的可維護性有幾層意思,首先的意思就是可以適應軟件在部署和使用中的各類狀況。從這個角度上來講,它對咱們的軟件提出的要求就是不能將代碼寫死。

  1)代碼不能寫死

  我曾經見個人同事將系統要讀取的一個日誌文件指定在C盤的一個固定目錄下,若是系統部署時沒有這個目錄以及這個文件就會出錯。若是他將這個決定路徑下的目錄改成相對路徑,或者經過一個屬性文件能夠修改,代碼豈不就寫活了。通常來講,我在設計中須要使用日誌文件、屬性文件、配置文件,一般都是如下幾個方式:將文件放到與類相同的目錄,使用ClassLoader.getResource()來讀取;將文件放到classpath目錄下,用File的相對路徑來讀取;使用web.xml或另外一個屬性文件來制定讀取路徑。

  我也曾見另外一家公司的軟件要求,在部署的時候必須在C:/bea目錄下,若是換成其它目錄則不能正常運行。這樣的設定經常爲軟件部署時帶來許多的麻煩。若是服務器在該目錄下已經沒有多餘空間,或者已經有其它軟件,將是很撓頭的事情。

  2)預測可能發生的變化

  除此以外,在設計的時候,若是將一些關鍵參數放到配置文件中,能夠爲軟件部署和使用帶來更多的靈活性。要作到這一點,要求咱們在軟件設計時,應當有更多的意識,考慮到軟件應用中可能發生的變化。好比,有一次我在設計財務軟件的時候,考慮到一些單據在製做時的前置條件,在不一樣企業使用的時候,可能要求不同,有些企業可能要求嚴格些而有些要求鬆散些。考慮到這種可能的變化,我將前置條件設計爲可配置的,就可能方便部署人員在實際部署中進行靈活變化。然而這樣的配置,必要的註釋說明是很是必要的。

  軟件可維護性的另外一層意思就是軟件的設計便於往後的變動。這一層意思與軟件的可變動性是重合的。全部的軟件設計理論的發展,都是從軟件的可變動性這一要求逐漸展開的,它成爲了軟件設計理論的核心。

  3. 可變動性

  前面我提到了,軟件的變動性是全部軟件理論的核心,那麼什麼是軟件的可變動性呢?按照如今的軟件理論,客戶對軟件的需求時時刻刻在發生着變化。當軟件設計好之後,爲應對客戶需求的變動而進行的代碼修改,其所須要付出的代價,就是軟件設計的可變動性。因爲軟件合理的設計,修改所付出的代價越小,則軟件的可變動性越好,即代碼設計的質量越高。一種很是理想的狀態是,不管客戶需求怎樣變化,軟件只需進行適當的修改就可以適應。但這之因此稱之爲理想狀態,由於客戶需求變化是有大有小的。若是客戶需求變化很是大,即便再好的設計也沒法應付,甚至從新開發。然而,客戶需求的適當變化,一個合理的設計可使得變動代價最小化,延續咱們設計的軟件的生命力。

  1)經過提升代碼複用提升可維護性

  我曾經遇到過這樣一件事,我要維護的一個系統由於應用範圍的擴大,它對機關級次的計算方式須要改變一種策略。若是這個項目統一採用一段公用方法來計算機關級次,這樣一個修改實在太簡單了,就是修改這個公用方法便可。可是,事實卻不同,對機關級次計算的代碼遍及整個項目,甚至有些還寫入到了那些複雜的SQL語句中。在這樣一種狀況下,這樣一個需求的修改無異於須要遍歷這個項目代碼。這樣一個實例顯示了一個項目代碼複用的重要,然而不幸的是,代碼沒法很好複用的狀況遍及咱們全部的項目。代碼複用的道理十分簡單,但要具體運做起來很是複雜,它除了須要很好的代碼規劃,還須要持續地代碼重構。

  對整個系統的總體分析與合理規劃能夠根本地保證代碼複用。系統分析師經過用例模型、領域模型、分析模型的一步一步分析,最後經過正向工程,生成系統須要設計的各類類及其各自的屬性和方法。採用這種方法,功能被合理地劃分到這個類中,能夠很好地保證代碼複用。

  採用以上方法雖然好,但技術難度較高,須要有高深的系統分析師,並非全部項目都能廣泛採用的,特別是時間比較緊張的項目。經過開發人員在設計過程當中的重構,也許更加實用。當某個開發人員在開發一段代碼時,發現該功能與前面已經開發功能相同,或者部分相同。這時,這個開發人員能夠對前面已經開發的功能進行重構,將能夠通用的代碼提取出來,進行相應的改造,使其具備必定的通用性,便於各個地方可使用。

  一些比較成功的項目組會指定一個專門管理通用代碼的人,負責收集和整理項目組中各個成員編寫的、能夠通用的代碼。這個負責人同時也應當具備必定的代碼編寫功力,由於將專用代碼提高爲通用代碼,或者之前使用該通用代碼的某個功能,因爲業務變動,而對這個通用代碼的變動要求,都對這個負責人提出了很高的能力要求。

  雖而後一種方式很是實用,可是它有些亡羊補牢的味道,不能從總體上對項目代碼進行有效規劃。正由於兩種方法各有利弊,所以在項目中應當配合使用。

  2)利用設計模式提升可變動性

  對於初學者,軟件設計理論經常感受晦澀難懂。一個快速提升軟件質量的捷徑就是利用設計模式。這裏說的設計模式,不只僅指經典的32個模式,是一切前人總結的,咱們能夠利用的、更加普遍的設計模式。

  a. if...else...

  這個我也不知道叫什麼名字,最先是哪位大師總結的,它出如今Larman的《UML與模式應用》,也出如今出如今Mardin的《敏捷軟件開發》。它是這樣描述的:當你發現你必需要設計這樣的代碼:「if...elseif...elseif...else...」時,你應當想到你的代碼應當重構一下了。咱們先看看這樣的代碼有怎樣的特色。

  Java代碼

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()中...通過以上的重構,代碼仍是這些代碼,效果卻徹底不同了。咱們只須要這樣寫:

  Java代碼

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)」。它在編寫時,在抽象類中並非一個抽象函數,但倒是一個什麼都不寫的空函數。繼承類在編寫時,若是須要這個步驟則重載這個函數,不然就什麼也不寫,進而在執行的時候也如同什麼都沒有執行。

  經過以上對模板模式的描述能夠發現,模板模式能夠大大地提升咱們的代碼複用程度。

  以上一些經常使用設計模式,都能使咱們快速提升代碼質量。仍是那句話,設計模式不是什麼高深的東西,偏偏相反,它是初學者快速提升的捷徑。然而,若是說提升代碼複用是提升代碼質量的初階,使用設計模式也只能是提升代碼質量的中階。那麼,什麼是高階呢?我認爲是那些分析設計理論,更具體地說,就是職責驅動設計和領域驅動設計。

  3)職責驅動設計和領域驅動設計

  前面我提到,當咱們嘗試寫一些複雜功能的時候,咱們把功能分解成一個個相對獨立的函數。可是,應當將這些函數分配到哪一個類中呢?也就是系統中的全部類都應當擁有哪些函數呢?或者說應當表現出哪些行爲呢?答案就在這裏:以職責爲中心,根據職責分配行爲。咱們在分析系統時,首先是根據客戶需求進行用例分析,而後根據用例繪製領域模式和分析模型,整個系統最主要的類就造成了。經過以上分析造成的類,每每和現實世界的對象是對應的。正由於如此,軟件世界的這些類也具備了與現實世界的對象相對應的職責,以及在這些職責範圍內的行爲。

  職責驅動設計(Responsibility Drive Design,RDD)是Craig Larman在他的經典著做《UML和模式應用》中提出的。職責驅動設計的核心思想,就是咱們在對一個系統進行分析設計的時候,應當以職責爲中心,根據職責分配行爲。這種思想首先要求咱們設計的全部軟件世界的對象,應當與現實世界儘可能保持一致,他稱之爲「低表示差別」。有了低表示差別,一方面提升了代碼的可讀性,另外一方面,當業務發生變動的時候,也能夠根據實際狀況快速應對變動。

  Craig Larman在提出職責驅動設計理論的同時,還提出了GRASP設計模式,來豐富這個理論。在GRASP設計模式中,我認爲,低耦合、高內聚、信息專家模式最有用。

  繼Craig Larman提出的職責驅動設計數年以後,另外一位大師提出了領域驅動設計。領域驅動設計(Domain Drive Design,DDD)是Eric Evans在他的同名著做《領域驅動設計》中提出的。在以前的設計理論中,領域模型是從用例模型到分析模型之間的一種中間模型,也就是從需求分析到軟件開發之間的一種中間模型。這麼一箇中間模型,既不是需求階段的重要產物,在開發階段也不以它做爲標準進行開發,僅僅是做爲參考,甚至給人感受有一些多餘。可是,Evans在領域驅動設計中,將它放到了一個無比重要的位置。按照領域驅動設計的理論,在需求分析階段,需求分析人員使用領域模型與客戶進行溝通;在設計開發階段,開發人員使用領域模型指導設計開發;在運行維護和二次開發階段,維護和二次開發人員使用領域模型理解和熟悉系統,並指導他們進行維護和二次開發。總之,在整個軟件開發的生命週期中,領域模型都成爲了最核心的內容。

  領域驅動設計繼承了職責驅動設計。在領域驅動設計中強調的,依然是低表示差別,以及職責的分配。可是,如何作到低表示差別呢?如何完成職責分配呢?領域驅動設計給了咱們完美的答案,那就是創建領域模型。領域驅動設計改變了咱們的設計方式。在需求分析階段,用例模型已再也不是這個階段的核心,而是創建領域模型。在開發和二次開發階段,開發人員也再也不是一埋頭地猛扎進程序堆裏開始編程,而是首先細緻地進行領域模型分析。領域驅動設計強調持續精化,使領域模型再也不是一旦完成分析就扔在一邊再也不理會的圖紙,而是在不斷理解業務的基礎上不斷修改和精化領域模型,進而驅動咱們代碼的精化。領域驅動設計強調的再也不是一次軟件開發過程當中咱們要作的工做,它看得更加長遠,它強調的是一套軟件在至關長一段時間內持續升級的過程當中咱們應當作的工做。我認爲,領域驅動設計是提升代碼質量的最高等級。當時,使用領域驅動設計進行軟件開發是一場至關巨大的改革,它顛覆了咱們過去的全部開發模式,咱們必須腳踏實地地一步一步去實踐和改變。

  職責驅動設計

  隨着軟件業的不斷髮展,隨着軟件需求的不斷擴大,軟件所管理的範圍也在不斷拓寬。過去一個軟件僅僅管理一臺電腦的一個小小的功能,而如今被擴展到了一個企業、一個行業、一個產業鏈。過去咱們開發一套軟件,只有少許的二次開發,當它使用到必定時候咱們就拋棄掉從新又開發一套。如今,隨着用戶對軟件依賴程度的不斷加大,咱們很難說拋棄一套軟件從新開發了,更多的是在一套軟件中持續改進,使這套軟件的生命週期持續數年以及數個版本。正是由於軟件業面臨着如此巨大的壓力,咱們的代碼質量,咱們開發的軟件擁有的可變動性和持續改進的能力,成爲軟件制勝的關鍵因素,令咱們不能不反思。

  代碼質量評價的關鍵指標:低耦合,高內聚

  耦合就是對某元素與其它元素之間的鏈接、感知和依賴的量度。耦合包括:

  1.元素B是元素A的屬性,或者元素A引用了元素B的實例(這包括元素A調用的某個方法,其參數中包含元素B)。

  2.元素A調用了元素B的方法。

  3.元素A直接或間接成爲元素B的子類。

  4.元素A是接口B的實現。

  若是一個元素過於依賴其它元素,一旦它所依賴的元素不存在,或者發生變動,則該元素將不能再正常運行,或者不得不相應地進行變動。所以,耦合將大大影響代碼的通用性和可變動性。

  內聚,更爲專業的說法叫功能內聚,是對軟件系統中元素職責相關性和集中度的度量。若是元素具備高度相關的職責,除了這些職責內的任務,沒有其它過多的工做,那麼該元素就具備高內聚性,反之則爲低內聚性。內聚就像一個專橫的管理者,它只作本身職責範圍內的事,而將其它與它相關的事情,分配給別人去作。

  高質量的代碼要求咱們的代碼保持低耦合、高內聚。可是,這個要求是如此的抽象與模糊,如何才能作到這些呢?軟件大師們告訴咱們了許多方法,其中之一就是Craig Larman的職責驅動設計。

  職責驅動設計(Responsibility Drive Design,RDD)是Craig Larman在他的經典著做《UML和模式應用》中提出的。要理解職責驅動設計,咱們首先要理解「低表示差別」。

  低表示差別

  咱們開發的應用軟件其實是對現實世界的模擬,所以,軟件世界與現實世界存在着必然的聯繫。當咱們在進行需求分析的時候,需求分析員其實是從客戶那裏在瞭解現實世界事物的規則、工做的流程。若是咱們在軟件分析和設計的過程當中,將軟件世界與現實世界緊密地聯繫到一塊兒,咱們的軟件將更加本色地還原事物最本質的規律。這樣的設計,就稱之爲「低表示差別」。

  採用「低表示差別」進行軟件設計,現實世界有什麼事物,就映射爲軟件世界的各類對象(類);現實世界的事物擁有什麼樣的職責,在軟件世界裏的對象就擁有什麼樣的職責;在現實世界中的事物,由於它的職責而產生的行爲,在軟件世界中就反映爲對象所擁有的函數。

  低表示差別,使分析設計者對軟件的分析和設計更加簡單,思路更加清晰;使代碼更加可讀,閱讀者更加易於理解;更重要的是,當需求發生變動,或者業務產生擴展時,設計者只須要遵循事物原本的面貌去思考和修改軟件,使軟件更加易於變動和擴展。

  角色、職責、協做

  理解了「低表示差別」,如今咱們來看看咱們應當如何運用職責驅動設計進行分析和設計。首先,咱們經過與客戶的溝通和對業務需求的瞭解,從中提取出現實世界中的關鍵事物以及相互之間的關係。這個過程咱們一般經過創建領域模型來完成。領域模型創建起來之後,經過諸如Rational Rose這樣的設計軟件的正向工程,生成了咱們在軟件系統中最初始的軟件類。這些軟件類,因爲每一個都扮演着現實世界中的一個具體的角色,於是賦予了各自的職責。前面我已經提到,若是你的系統採用職責驅動設計的思想進行設計開發,做爲一個好的習慣,你應當在每個軟件類的註釋首行,清楚地描述該軟件類的職責。

  當咱們完成了系統中軟件類的制訂,分配好了各自的職責,咱們就應該開始根據軟件需求,編寫各個軟件類的功能。在前面我給你們提出了一個建議,就是不要在一個函數中編寫大段的代碼。編寫大段的代碼,一般會下降代碼的內聚度,由於這些代碼中將包含不是該軟件類應當完成的工做。做爲一個有經驗的開發人員,在編寫一個功能時,首先應當對功能進行分解。一段稍微複雜的功能,一般均可以被分解成一個個相對獨立的步驟。步驟與步驟之間存在着交互,那就是數據的輸入輸出。經過以上的分解,每個步驟將造成一個獨立的函數,而且使用一個能夠代表這個步驟意圖的釋義函數名。接下來,咱們應當考慮的,就是應當將這些函數交給誰。它們有可能交給原軟件類,也有可能交給其它軟件類,其分配的原則是什麼呢?答案是否清楚,那就是職責。每一個軟件類表明現實世界的一個事物,或者說一個角色。在現實世界中這個任務應當由誰來完成,那麼在軟件世界中,這個函數就應當分配給相應的那個軟件類。

  經過以上步驟的分解,一個功能就分配給了多個軟件類,相互協做地完成這個功能。這樣的分析和設計,其代碼必定是高內聚的和高可讀性的。同時,當需求發生變動的時候,設計者經過對現實世界的理解,能夠很是輕鬆地找到那個須要修改的軟件類,而不會影響其它類,於是也就變得易維護、易變動和低耦合了。

  說了這麼多,舉一個實例也許更能幫助理解。拿一個員工工資系統來講吧。當人力資源在發放一個月工資的時候,以及離職的員工確定不能再發放工資了。在系統設計的期初,開發人員商量好,在員工信息中設定一個「離職標誌」字段。編寫工資發放的開發人員經過查詢,將「離職標誌」爲false的員工查詢出來,併爲他們計算和發放工資。可是,隨着這個系統的不斷使用,編寫員工管理的開發人員發現,「離職標誌」字段已經不能知足客戶的需求,於是將「離職標誌」字段廢棄,並增長了一個「離職時間」字段來管理離職的員工。然而,編寫工資發放的開發人員並不知道這樣的變動,依然使用着「離職標誌」字段。顯然,這樣的結果就是,軟件系統開始對離職員工發放工資了。仔細分析這個問題的緣由,咱們不難發現,確認員工是否離職,並非「發放工資」軟件類應當完成的工做,而應當是「員工管理」軟件類應當完成的。若是將「獲取非離職員工」的任務交給「員工管理」軟件類,而「發放工資」軟件類僅僅只是去調用,那麼離職功能由「離職標誌」字段改成了「離職時間」字段,其實就與「發放工資」軟件類毫無關係。而做爲「員工管理」的開發人員,一旦發生這樣的變動,他固然知道去修改本身相應的「獲取非離職員工」函數,這樣就不會發生以上問題。經過這樣一個實例,也許你可以理解「職責驅動設計」的精要與做用了吧。

  職責分配與信息專家

  經過以上對職責驅動設計的講述,咱們不難發現,職責驅動設計的精要就是職責分配。可是,在紛繁複雜的軟件設計中,如何進行職責分配經常令咱們迷惑。幸運的是,Larman大師清楚地認識到了這一點。在他的著做中,信息專家模式爲咱們提供了幫助。

  信息專家模式(又稱爲專家模式)告訴咱們,在分析設計中,應當將職責分配給軟件系統中的這樣一個軟件類,它擁有實現這個職責所必須的信息。咱們稱這個軟件類,叫「信息專家」。用更加簡短的話說,就是將職責分配給信息專家。

  爲何咱們要將職責分配給信息專家呢?咱們用上面的例子來講明吧。當「發放工資」軟件類須要獲取非離職員工時,「員工管理」軟件類就是「獲取非離職員工」任務的信息專家,由於它掌握着全部員工的信息。假設咱們不將「獲取非離職員工」的任務交給「員工管理」軟件類,而是另外一個軟件類X,那麼,爲了獲取員工信息,軟件類X不得不訪問「員工管理」軟件類,從而使「發放工資」與X耦合,X又與「員工管理」耦合。這樣的設計,不如直接將「獲取非離職員工」的任務交給「員工管理」軟件類,使得「發放工資」僅僅與「員工管理」耦合,從而有效地下降了系統的總體耦合度。

  總之,採用「職責驅動設計」的思路,爲咱們提升軟件開發質量、可讀性、可維護性,以及保持軟件的持續發展,提供了一個廣闊的空間。

相關文章
相關標籤/搜索