讀代碼整潔之道

 現在的軟件系統開發難度主要在於其複雜度和規模,客戶需求也不再像Winston Royce瀑布模型期望那樣在系統編碼前完成所有的設計滿足用戶軟件需求。在這個信息爆炸技術日新月異的時代,需求總是在不停的變化,隨之在2001年業界17位大牛聚集在美國猶他州的滑雪勝地雪鳥(Snowbird)雪場,提出了“Agile”(敏捷)軟件開發價值觀,並在他們的努力推動下,開始在業界流行起來。在《代碼整潔之道》(Clean Code),提出一種軟件質量,可持續開發不僅在於項目架構設計,還與代碼質量密切相關,代碼的整潔度和質量成正比,一份整潔的代碼在質量上是可靠的,爲團隊開發,後期維護,重構奠定了良好的基礎。在這本書中作者提出了注重實際開發實踐的細節,而不是站在空洞的理論來談論整潔之道。

什麼是整潔代碼?不同的人會站在不同的角度闡述不同的說法。而我最喜歡的是Grady Booch(《面向對象分析與設計》作者)闡述:

    “整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了乾淨利落的抽象和直截了當的控制語句。”

      整潔的代碼就是一種簡約(簡單而不過於太簡單)的設計,閱讀代碼的人能很清晰的明白這裏在幹什麼,而不是隱澀難懂,整潔的代碼讀起來讓人感覺到就像閱讀散文-藝術的沉澱,作者是精心在意締造出來。

一:命名

     命名包括變量、函數、參數,類等,一個好的命名能夠很好的表述其所承載的業務,從命名上就已經很好的答覆了爲什麼存在,做了什麼事,應該怎麼用等的大部分的問題,閱讀者看到它的時候不必去深究其實現細節,一切都在命名上一目瞭然。一個好的命名必須是名副其實,不存在歧義(雙關語或常見屬於衝突),直接了當(否定語句或者誤導性命名)。

二:函數:

     從彙編/C時代開始的到現在函數一直都存在與我們開發中不可或缺的一部分,結構化組織,重用.作爲函數式語言的一等公民,所有程序的第一組代碼。

  1. 好的函數必須足夠的小,其次還是足夠的小。很容易想像閱讀上千行的代碼,是多麼巨大的自我心理挑戰,在實習的時候工作於毫無分層邏輯的WinForm平臺下,完全依賴RAD模式帶來後置cs頁面上千行的代碼,每次修改都令我惱怒,恨不得重寫整個業務邏輯。

  2. 一個函數在於短小精悍,只作一件事情,並做好這件事,只做一件事才能得到更好的利用函數名錶述自己。

  3. 好的函數還應該是CQS(查詢命令分離)無副作用的(不存在隱藏歧義的背後邏輯),並對其他類型不存在“依戀情節(Feature Envy)“(類中的變量被所有的函數使用這是理想的高內聚,萬物皆有其位,而後物盡歸其位)。

  4. 函數的參數應該足夠的少,無最好,一次之,再次爲二,儘量避免三個以及三個以上,對於太多的參數你可能該採用IntroduceParameterObject(引入參數對象)。

  5. 重複的代碼。重複在軟件系統是萬惡的,我們熟悉的分離關注點,面向對象,設計原則…都是爲了減少重複提高重用,Don’t repeat yourself!(DRY)。

三:註釋、格式:

    並不是寫出完備的註釋就是好的開發人員,如果代碼清晰的表述自己意圖,那麼註釋反而多餘。在《重構-改善現有代碼之道》中Martin Fowler指出多餘的註釋是一種代碼壞味道。就是好的註釋隨着項目的維護不斷的重構很多時候也會變得不那麼適應,而我們很少會去主動維護。再則誤導性的註釋更爲使用者所憎恨。當然有時我們也得使用註釋,註釋並不是萬惡的,當我們沒法用代碼來描述自己的時候,我們需要註釋去描述意圖;多餘有副作用的代碼給使用者提供警告註釋。TODO開發時進度控制,比如你在進行較大規模領域重構,目前有些邏輯不再適應,不那麼自然,而對它的重構還在任務列表最後,你可以選擇標註在TODO中,最後完成從ToDoList中去掉每一個TODO任務。

   良好的代碼格式,會使得我們閱讀更容易,一套共同的格式會讓我們查找理解更快速。每個團隊都應該遵循一套固定的代碼格式規範,整個軟件系統的統風格統一,而不是各自爲政各成一體。

四:對象和數據結構:

   數據結構指的就是數據的載體,暴露數據,而幾乎沒有有意義的行爲的貧血類。最常見的應用在分佈式服務,以wcf,webservice,reset之類的分佈式服務中不可或缺的數據傳輸對象(DTO)模式,DTO(Request/Response)就是一個很典型的數據載體,只存在簡單的get,set屬性,並且更傾向於作爲值對象存在。而對象則剛好相反作爲面向對象的產物,必須封裝隱藏數據,而暴露出行爲接口,DDD中領域模型傾向於對象不僅在數據更多暴露行爲操作自己或者關聯狀態。

   數據結構和對象之間看是細微的差別卻導致了不同的本質區別:使用數據結構的代碼便於在不改動現在數據結構的前提下添加新的行爲(函數),面向對象代碼則便於不改動現 有函數的前提下添加新的類。換句話說就是數據結構難以添加新的的數據類型,因爲需要改動所有函數,面向對象的代碼則難以添加新的函數,因爲需要修改所有的類。在任何一個複雜的系統都會同時存在數據結構和對象,我們需要判斷的是我們需要的是需要添加的新的數據類型還是新的行爲函數。

  隱藏作爲面向對象主要特性中的最重要特性,封裝隱藏是面向對象中最重要的特性,一個好的面向對象代碼肯定是對對象的內部細節做到很好的隱藏封裝,封裝過後纔有是多態,委派之類的。一個好的面向對象的代碼一定是具有很好的隱藏封裝,易於測試,不穩定因素往往集中在一處很小或者固定的位置,不穩定因素的變更不會導致更大面積的修改擴散。

 對象的隱藏要求:方法不應和任何調用方法返回的對象操作,換句話之和朋友說話,不和陌生人說話(迪米特法則,或被譯爲最小知識原則),比如:ctxt.getOptions().getSearchDir().getAbsolutePath(),就是迪米特法則的反例模式。

五:異常處理:

  每個軟件系統都避不開異常處理,需要防止它搞亂我們的邏輯。

  1. 利用異常處理代替返回異常編碼,返回異常編碼會是的代碼中充滿了if/else,switch/case擾亂我的代碼流轉。

  2. 對於特定異常撲捉,可以面向異常編程,編寫特定的異常類,使得對異常封裝轉化,更容易捕善後獲處理。

  3. 避免返回null,在軟件系統中最常見頭疼的就是NullReferenceException。在非特定場景下,我們應該極力的避免返回null。面對這種場景我們可以採用null object Pattern(空對象模式)返回特例對象,如c#類庫中的Guid.Empty,string.Empty;對於集合類型我們可以返回長度0的空集合而非null;

六:邊界:

  在系統開發中不可能一切都得從零開始,自己寫所有的代碼,更好的方案是需要整合一些開源或者第三方的項目,爲我所用。但是不能讓這些非自己的代碼滲侵中我們的代碼各處,有一些所以功能很強大的第三方產品,但不一定具有很好的抽象。很多時候我更寧願花些時間抽象出我們自己所需要的接口在第三方類庫上外覆一層自己的抽象,這樣不僅便於TDD,因爲我們能夠很好的創建僞對象,使的測試獨立不依賴外部資源,得到快速反饋;而且在設計上得到很好的擴展,當由於某些原因如第三方類庫不再能滿足業務需求,或者權益收費等等,我們可以很好的切換底層而使得修改不會擴散到系統各處。外覆類也是處理遺留代碼帶入測試容器的一種很好實踐。

七:單元測試:

   TDD中測試代碼在往往和產品代碼差不多,在系統中佔據一半的代碼量,不好的測試代碼也可能拖累項目的開發。整潔的測試代碼應該是遵循first原則的:

  1. 快速(Fast):測試應該快速,因爲需要不斷的運行測試得到反饋,我們需要的快速反饋,錯誤的快速定位。所以你的測試就不能依賴太多的外部資源,數據庫,硬件環境等等,對於這些外部資源應該採用僞對象模式來隔離。

  2. 獨立(Independent):測試應該是獨立的,獨立於測試用例之間,獨立於特定的環境,獨立於測試的運行順利。數據的獨立通常採用兩種獨立方式,每個測試環境的獨立,很多時候我們希望每個測試運行完成後環境(如數據庫)和運行前保持一致,如數據庫高層次測試我們更希望在每次測試完成後不會帶來多餘或者改變數據。再則就是數據的隔離,我們的行爲測試(BDD,集成高角度的測試)都會依賴一些固定的信息,通常是登陸系統的人員,我們可以採用麼個測試建立一個不同的登陸人員來使的每個測試之間的s數據隔離。

  3. 可重複(Repeatable):測試應該可以在任何環境下可重複,可運行,因爲測試獨立於環境外部資源。

  4. 自足驗證(Self-Validation):測試應該有通過失敗的標示,從每一個測試上能得到一處代碼邏輯的通過失敗。每個測試都有對同一件事物的一種行爲的斷言,也之斷言一件事,從而能夠很好的錯誤定位,避免高技巧性的測試。

  5. 及時(Timely):測試應該是及時編寫的,TDD要求測試必須在實現代碼之前,提前以使用者的角度定義使用接口方式。如果你是在編碼後補測試,你的測試覆蓋很可能不夠,而且容易定式於實現的邏輯寫測試,很多時候對於較低層次的測試也不是那麼容易寫的。一個設計良好的代碼必須也是可測試的。

八:類:

    面向對象的相似行爲的抽象,函數代碼塊的組織形式,在面向對象中我們的軟件系統是由衆多的類和類之間的交互協作完成了。面向對象特徵:封裝,繼承,多態度,委派。一個設計良好的類該是具有良好的封裝,站在使用者的調度考慮那些是使用接口,那些是內部細節;這是面向對象最主要的特徵,但是有時會與測試衝突,可以適當的放開並僅限於於測試調用。繼承和多態在面向對象中可以實現重用,但我更傾向於繼承不是爲了重用,而是隔離變化;大量的濫用繼承不乾淨的繼承體系將會導致龐大的繼承體系,繼承體系中衆多職責重複在各個同級派生類,理想的繼承應該是滿足里氏替換原則(LSP:每個父類出現的地方都應該可以被派生類所替換,並且能正確的工作);面oo第二原則組合優先。而委派則是一個類把部分功能委派給其他類來完成,體現類之間的協作,類似組合。

    1. 類第一原則應是是小並足夠的小。但與函數不同的是函數以代碼行數統計,而類以權責統計。

    2. 單一原則(SRP),體現了類只應該做一件事,並且做好它,這樣變化修改的理由只有他所做的事。良好的軟件設計中系統是由一組大量的短小的類和他們之間功能協作完成的,而不是幾個上帝類。

    3. 內聚:高內聚低耦合:提出與結構化編程,內聚表述模塊內部功能不同操作邏輯之間的距離,如果一個類的每個變量都被每個方法所使用爲最大的內聚;耦合描述模塊之間的依賴程度;高內聚低耦合以簡單的方式表述就是功能完備(高內聚)對象之間是通過穩定的接口(低耦合)交互的。

    4. 依賴倒置(DIP):描述組件之間高層組件不應該依賴於底層組件。依賴倒置是指實現和接口倒置,採用自頂向下的方式關注所需的底層組件接口,而不是其實現。DI模式很好的就是應用IOC(控制反轉)框架,構造方式分爲分構造注入,函數注入,屬性注入;.net平臺流行的IOC框架有Unity,Castle windsor,Ninject,Autofac等框架支持,

九:併發編程:

    併發是一種時間(When)和目的(What)的解耦,提供應用程序的吞吐量,提高cpu利用率;但是併發編碼不是那麼容易,再加上臨界資源競爭死鎖。在併發編程的時候我們必須儘量遵守一些原則:

  1. 併發已經足夠複雜,我們更需要代碼分離,分離線程相關代碼和非線程相關代碼,單一權責,儘可能降低其複雜度。

  2. 限制臨街資源的作用域,爲臨界資源加鎖是防止併發的策略,但是必須正確的加鎖,如果形成等待環,就導致死鎖。

  3. 利用數據副本(值對象或者克隆)在線程之間傳遞數據,避免線程之前操作的併發影響;線程獨立,使其在自己的環境中運行,不能其他線程共享數據。

  4. 對於臨界資源加鎖應儘量保持加鎖範圍儘可能的小。

http://www.cnblogs.com/whitewolf/archive/2012/07/31/2617473.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章