有關代碼複用

重用代碼的目標是什麼?(我們爲什麼要重用代碼)

:避免重複發明輪子,有大量的已經寫好的功能可以調用.這也是OO(面向對象的目標)

什麼樣的代碼是可以複用的代碼?

 

1.接口良好的.(利於用戶閱讀和使用)

         1).類的接口應該展現一致的抽象層次.

         public class EmpolyeeContainer extends LinkedList{

        public void addEmpolyee(Empolyee e);//1

        public void removeEmpolyee(Empolyee e);//2

       

        public Empolyee nextItem();//3

        public Empolyee firstItem();//4

        public Empolyee lastItem();//5

}

這個接口暴露了兩層抽象1,2Container操作,3,4,5List操作.實際上我們只需要1,2.這裏不應該用繼承而應該使用組合.這樣做會暴露內部的實現機制,造成客戶調用的混亂,

public class EmpolyeeContains{

        public void addEmpolyee(Empolyee e);//1

        public void removeEmpolyee(Empolyee e);//2

        public Empolyee nextEmpolyee();

       

        private LinkedList innerContainer ;//3

}

1,2的操作通過3的數據結構實現,但是接口本身不暴露內部的實現機制.暴露的內部細節越少越有利於修改.

2).提供成對()的服務.

3).儘可能讓接口可編程,而不是表達語義.

暴露的接口應該僅僅通過編譯器就能限制其使用方法,例如接口限定方法的參數類型,個數等等編譯器可檢查的錯誤.如果接口中有這樣的方法step1(),step2(),隱含的表達step1必須在step2之前調用否則會出錯,這種語義性的行爲是編譯器無法檢查的.也就是說調用者有可能會調用錯誤.應該儘量避免這種接口設計,至少要通過註釋文檔說明這種語義.

commons-lang包中的StringUtils,這是個常用的字符串處理工具類,這是個設計很好的接口基本上每個方法的名字就說明了自己的行爲,方法之間也沒有語義的約束.而且方法往往分組,一些列的substringBetween,substringBefore,substringAfter.

2.代碼是可讀的.(利於用戶和自己理解代碼)

可以工作的代碼,已經暴露的良好的接口,用戶可以很方便的使用,而且他沒必要了解內部實現,所以內部代碼未必需要可讀只要能工作就行.這種想法是錯誤(愚蠢).因爲沒有任何代碼是一成不變的,需求總在變化,總有一天需要修改代碼以適用新的需求,否則代碼的生命週期就結束了也就不再可複用.

改善代碼可讀性的原則.

1).前面提到過的.統一的抽象層次有利於代碼的閱讀.

2).良好設計編碼的子程序(方法)

         只做一件事(單一職權原則)

         這意味着方法不應該有副作用,考慮如下代碼.

public boolean validateUser(User user){

    session.init();

    if(user.getPassword().equals(password)){

        return true ;

    }else{

        return false ;

    }

}

         問題就出在session.init().方法名並沒有暗示,方法會初始化session.方法名可以改爲initSessionAndValidateUser(User user)但這依然違反了單一職權原則.但是卻可以讓用戶清楚知道代碼有這樣的副作用.

         方法不易過長.語句之間應保持統一的抽象(這個比較難做到,也難以理解).

30秒原則(任何方法如果在30秒內不能閱讀理解,就應當做進一步拆分).

private HttpUriRequest buildHttpPost() {

    String uri = uriPattern.replaceAll("\\?.*", "");//1

    logger.info("POST請求:\n{}", uri);//2

    HttpPost req = new HttpPost(uri);//3

    HttpEntity entity = buildHttpEntity();//4

    req.setEntity(entity);//5

    return req;

}

3,4,5處於統一抽象層次,每一句都是描述如何構建一個HTTP_POST請求.但是1,2就不是,他們描述的是如何得到正確的請求URI.這兩句可以進一步拆分成createPostUri().但是目前爲止方法本身符合30秒原則,所以可讀性是可以接受的,但如果未來由於需求變化,要做進一步的修改時,這個方法就必須做進一步拆分.

參數的數量.

大多數情況下,無參數或單參數的情況最好.例外的情況比如Point(x,y)這樣的方法,Point具有其自然屬性,所以兩個參數比較合適.對於單參數方法最好方法名與參數名形成動詞/名詞形式.例如write(name)這個方法如果寫成writeField(name)那麼調用者就很容易知道name是一個field.

標示參數,有些方法使用標示參數比如有這樣一個方法newWriter(File file , boolean append)appendtrue時新建的Writer會將內容附加到file,否則清空原來的文件寫入內容.這時起碼就要閱讀以下文檔調用者才知道如何使用標示參數.這是可以採用拆分爲兩個方法來去掉這個標示參數.newWriter(File file),newAppendableWriter(File file).這樣就一目瞭然了.

輸出參數是不建議使用的,應該儘量使用返回值做爲輸出結果.

變量和方法名稱應當有意義,做到代碼本身就是自解釋的.

推薦書籍<Clean Code>代碼整潔之道.代碼大全,敏捷軟件開發

3.遵循編碼實踐.

         有很多編碼的實踐可以借鑑,以提高代碼質量.例如經典的設計模式.(由於篇幅有限再次就不介紹了).在此只介紹一些我在日常工作中的體會.

         1).面向接口編程,避免使用繼承而使用對象組合.依賴倒置(依賴注入)

         面向接口編程無疑可以減少對具體類的依賴,降低類之間的耦合.良好設計的接口是細小而穩定的,那麼依賴接口可以減少對類的改變(符合開放封閉原則).

繼承是OO中很方便的複用代碼的方式,雖然Java只支持單繼承,降低了繼承的複雜度,但當繼承層次增加,代碼的可讀性就會下降,內部細節也越複雜子類對父類的依賴和耦合也比較嚴重.特別是子類使用父類的保護成員和字段的時候,這樣的類在閱讀代碼的時候是很晦澀的.

對象組合技術同樣可以實現繼承帶來的功能(雖然有時要多些一些代碼,但是可以讓程序結構更簡單靈活)

觀察如下代碼.

abstract class AbstractExampleDocument
{
    // skip some code ...
    public void output(Example structure)
        {
            if( null != structure )
                {
                this.format( structure );
                }
        }
    protected void format(Example structure);
}

當要實現自定義格式的時候只需要繼承該類並覆蓋format保護方法即可.此時由於需求變化要在該類中添加一個將格式化好的structure保存到數據庫的功能.但父類不知道子類如何格式化structure所以無法確定保存到數據庫的實現.可以考慮在父類中增加一個save()抽象方法,待子類去實現.天啊,所有的子類都要實現一次save()方法.而且所有的格式化操作和保存到數據庫的操作是緊密耦合的.structure保存到數據庫的操作可能在很多地方都有用,但是卻無法從子類中單獨提取出來使用.

        

class DefaultExampleDocument 

{

private Formatter formatter ;

 

public void setFormatter(Formatter formatter){

    this.formatter = formatter;

}
    // skip some code ...
    public void output(Example structure) 
    {
     ExampleFormatter formatter = 
       (ExampleFormatter) manager.lookup(Roles.FORMATTER);
     if( null != structure ) 
     {
       formatter.format(structure);
     }
  }
}

使用對象組合技術,只需要再編寫一個Saver放到DefaultExampleDocument ,output結束之前調用Saversave方法即可應對上述變化需求.此時依賴注入出場了,什麼樣formatter對應什麼樣的saver完全交由依賴注入決定,而且硬編碼在一起,解決了耦合問題.而且對DefaultExampleDocument 的修改完全不需要影響所有的formatter.

可見優先使用對象組合,和麪向接口編程可以很好的利用依賴注入實現代碼的解耦提高代碼的可維護性和可擴展性.

         繼承也不是一定不要用的,當父類和子類有is-a關係時候,適合使用繼承.使用繼承時要注意符合替換原則(在任何時候子類實例都可以替換父類實例使用)

單一職權和開放封閉原則.

該原則要求代碼對擴展是開放的,對修改是封閉的.最佳狀態是一個類或者方法,只有一個引起變化的理由.當然要做到這一點首先就是要符合單一職權原則(只做一件事).符合單一職權原則纔可以保證只要在要做的那件事的需求有變化的時候纔會修改代碼.否則只需要擴展代碼.實現開放封閉的方式有很多,例如前面的對象組合技術,還有很多有關的設計模式Bridge,Composite,Decorator,Observer,Strategy等等.

case/if語句

這種語法是大多編程語言中的基本流程控制語句.常常造成過多的縮進,和過多的流程分支.而且這種語句難以拆分成小方法.嚴重影響代碼的可讀性.當分支過多時,是不是可以考慮使用查表法?

例如,在分析xml文件時對不同的標籤初始化不同的對象實例

TagInstance instance ;

if(tag==A){

    instance = new A();

}else if(tag==B){

    instance = new B();

}else if(tag ==C){

    instance = new C() ;

}else{

    instance = new D();

}

可以替換爲

Map map = new HashMap();

map.put(“A”,new A());

map.put(“D”,new D());

TagInstance instance = map.get(tag);

 

這樣明顯邏輯清楚而且很容易修改,代碼量也下降了.也很容易進行方法拆分.

 

做到上述的代碼就是可複用的好代碼嗎?未必,作爲一個開發人員編寫好代碼是貫徹始終的,所有的實踐均來源於自己和他人的經驗總結,到底如何才能寫出好的可複用的代碼每個人的標準也不完全一致.在此只是提及了一些我認爲是大多數人公認的原則和實踐.

參考資料,代碼整潔之道,代碼大全,敏捷軟件開發.

發佈了26 篇原創文章 · 獲贊 6 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章