編寫高質量的代碼(1)

對於不能肯定的,就需要去判斷

 

專業性和技藝來自於驅動規程的價值觀。——>整潔的代碼

 

首先要從宏觀上想清楚要做哪幾件事

添加只需要做少量修改,且修改是隔離的——>需要使用繼承和多態

優秀的軟件設計,大都關乎分隔——創建合適的空間放置不同種類的代碼。對關注面的分隔讓代碼更易於理解和維護!

僅僅讓代碼能工作是不專業的。要改進代碼的結構和設計。

糟糕的代碼可以清理。不能讓模塊之間互相滲透,出現大量隱藏糾結的依賴關係

保持代碼持續整潔和簡單

 

儘可能消除重複:

每次看到重複代碼,都代表遺漏了抽象。重複的代碼可能成爲子程序或乾脆是另一個類。將重複代碼疊放進類似的抽象,增加了你的設計語言的詞彙量,提升了抽象層級。

重複最明顯的形態就是不斷看到明顯一樣的代碼,不斷在複製粘貼代碼,可以用單一方法來替代之

較隱蔽的形態是在不同模塊中不斷重複出現、檢測同一組條件的switch/case或if/else鏈。可以用多態來替代之。

更隱蔽的形態是採用類似算法但具體代碼行不同的模塊,這也是重複,可以使用模板方法模式或策略模式來修正。

 

設計模式是消除重複的有效手段。 範式是消除數據庫設計中的重複的策略。oo自身也是組織模塊和消除重複的策略。

 

創建分離較高層級一般性概念與較低層級細節概念的抽象模型,分離要完整;基類對派生類應該一無所知。

將派生類和基類部署到不同的jar文件中,確保基類jar文件對派生類jar文件的內容一無所知。這樣就能將系統部署爲分散和獨立的組件。這樣,修改產生的影響就降低了,維護系統變得更簡單。

孤立抽象是開發者很難做到的事之一;

 

耦合==依賴,耦合度低,就是依賴少;

優秀的軟件開發人員要學會限制類或模塊中暴露的接口數量類中的方法越少越好函數知道的變量越少越好類擁有的成員變量越少越好

隱藏你的數據。隱藏你的工具函數。隱藏你的常量。

 

在一個函數中不要有選擇性參數,選擇性參數可能是boolean類型,可能是枚舉元素、整數或任何一種用於選擇函數行爲的參數。

使用多個函數,通常優於向單個函數傳遞某些代碼來選擇函數行爲

 

讓程序可讀的最有力方法之一就是將計算過程打散成在用有意義的單詞命名的變量中放置的中間值。解釋性變量多比少好

函數名稱應該表達其行爲。若必須查看函數的實現才知道它是做什麼的,就該換個更好的函數名

 

花時間理解算法。在你認爲自己完成某個函數之前,確認自己理解了它是怎麼工作的

 

位置錯誤的職責

 

用多態代替if/else或switch/case。 

命名常量替代沒有意義的符號,如原始形態數字、字母

 

在代碼中要足夠準確明確自己爲何要這麼做,若遇到異常情況如何處理。若調用可能返回null的函數,確認自己檢查了null值。

 

結構甚於約定。不能隨意。

封裝條件。因爲布爾邏輯難以理解。

函數只做一件事。

每個函數都產生下一個函數所需的結果——>時序性;

 

在較高層級放置可配置數據。

不要繼承常量,而是用靜態導入

 

避免傳遞瀏覽

讓協作者提供所需的全部服務,不要讓模塊瞭解太多其協作者的信息。即若A與B協作,B與C協作,不要讓使用A的模塊瞭解C的信息,如不要寫類似a.getB().getC().doSomething()的代碼。

 

使用枚舉來替代常量。別再用public static final int,因爲enum可以擁有方法和字段,從而成爲能比int提供更多表達力和靈活性的強有力工具。

 

採用描述性名稱。事物的意義隨着軟件的演化而變化,因此要經常性重新估量名稱是否恰當

名稱要與抽象層級相符。

儘可能使用標準命名法。

具有與項目有關的特定意義的名稱用得越多,讀者就越容易明白你的代碼是做什麼的。

 

原則:

a、稍後等於永不!

b、離開時要比發現時更整潔!持續改進!如只是改好一個變量名,拆分一個有點過長的函數,消除一點點重複代碼,清理一個嵌套if語句。

c、沉迷測試!——>寫出整潔代碼。

d、具有表達力且短小精悍。 

e、命名——首先描述其作用,是幹什麼的,然後再定名字! 

f、測試使得改動變爲可能

g、若有些函數想要共享某些變量,就讓它們擁有自己的類

h、將大函數拆分爲小函數,往往也是將類拆分爲多個小類的時機

i、對類的任何修改(包括打開類)都有可能破壞類中的其它代碼。 但對類進行擴展(也是有修改),就不會破壞類中的其它代碼。

j、類應當對擴展開放,對修改關閉。通過子類化手段,對添加新功能是開放的,而且不影響其它類。

k、希望將系統打造成在添加或修改特性時儘可能少惹麻煩。系統通過擴展系統而非修改現有代碼來添加新特性。(多態

l、使用依賴注入、接口和抽象儘可能減少耦合,如此設計就會進步。

m、重複有多種表現,極其雷同的代碼行也是重複類似的代碼往往可以調整得更相似。重複也有實現上的重複。 

n、做了一點點共性抽取,此時已經違反了SRP原則,因此可以把一個新方法分解到另外的類中。小規模複用可降低系統複雜性。

o、模板方法模式是一種移除高層級重複的通用技巧。

p、要編寫整潔代碼,必須先寫骯髒的代碼,然後再清理它。

q、儘量減少函數參數

r、放進拿出是重構過程中常見的事

s、if條件判斷應該封裝成一個方法,用來解釋這個條件判斷,這樣可以更清晰表達代碼意圖;

t、以重用爲原則,若不能完全重用,就部分重用,不能重用部分就分出去;重用的一定要具有原子性,即不可再分

u、嚴格分層,劃分每層職責,將自己這層做好,然後再去調用下一層

v、每次修改都要問自己爲什麼要這樣改,把修改的原因寫下來;

w、設計良好的模塊有着非常小的接口,不提供許多需要依靠的函數,因此耦合度也較低

x、不相互依賴的東西不該耦合。如普通的enum不應在特殊類中包括,因爲這樣應用程序就要了解這些更爲特殊的類。根源是將變量、常量或函數不恰當放在臨時方便的位置。應該要花點時間研究應該在什麼地方聲明函數、常量和變量。

y、開發者最重要的決定之一就是在哪裏放代碼——代碼應該放在讀者自然而然期待它所在的地方。先想想爲什麼要放在這不放在這行不行

z、通常傾向於選用非靜態方法,若的確需要靜態函數,要確保沒機會打算讓它有多態行爲。

 

 

知和行:知就是熟悉有關原則、模式和實踐的知識;只有通過實踐才能叫掌握。

閱讀大量代碼,琢磨某段好在什麼地方、壞在什麼地方。

編寫閱讀清理代碼時思維方式的知識庫

以作者的思維路徑考慮問題。

 

寫代碼要循序漸進,即寫一段代碼就運行檢驗一下;

忽略掉的代碼部分就是缺陷藏身之地; 

必須要保證每個輸入,都有輸出;要有健壯性,即輸入爲null時,也要保證輸出正常!

 

對於程序員來說,(邏輯)簡單、易讀、高可維護性和(業務方法)複用性,是最重要的!

a、恰當命名標識至關重要。

b、每段代碼都該在你希望它所在的地方,若不在那裏,就需要重構了。

c、對於四處遺棄的帶註釋的代碼及反映過往或期望的無註釋代碼——除之而後快。

d、標準化——一貫的代碼風格

e、自律——在實踐中貫徹規程。


100W行代碼,質的變化!——還得練!!!!

 

《敏捷軟件開發:原則、模式與實踐》:關注面向對象設計的原則以及開發者的實踐方法

《代碼整潔之道》
 

學習好程序員的思維過程,使用的技巧、技術和工具!

 

影響軟件質量的因素:架構、項目管理、代碼質量;

無論是架構還是代碼都不強求完美,只求竭誠盡力而已。

 

代碼質量和整潔度成正比。

乾淨的代碼,在質量上可靠,爲後期維護、升級奠定了良好的基礎。

 

1、In Action

命名、函數、註釋、代碼格式、對象和數據結構、錯誤處理、邊界問題、單元測試、類、系統、併發編程等做到整潔的最佳實踐。 

 

(1) 整潔代碼:

如何編寫好代碼?如何將糟糕的代碼改寫成好代碼?——消除重複、只做一件事、提高代碼表達力、提早構建簡單抽象(小規模抽象),時時保持代碼整潔;

 

關注模型和需求!需求能作爲代碼的可執行測試來使用。細節(代碼)必須明確

 

代碼:就是用特定語言編寫的規約,是最終用來表達需求的語言。它必須嚴謹、精確、規範、詳細,好讓機器理解和執行!

對代碼的每次修改都影響到其它兩三處代碼。

要熟悉系統設計,這樣修改才能符合設計意圖。

花時間保持代碼整潔關乎生存

 

用戶指望需求、功能是否都在系統中實現,PM從我們這裏得到必須的信息,我們不應該羞於告知自己的想法!PM奮力衛護進度和需求,coder奮力衛護code!

程序員不能遵從不瞭解混亂風險的PM的意願

 

做得快的唯一方法——始終儘可能保持代碼整潔!之前的混亂會拖自己的後腿!

 

寫整潔代碼,需要遵守大量的小技巧!只有實踐,才能得到這種‘代碼感’。有了這種代碼感,就能從混亂中看到其它的可能與變化。

 

什麼是整潔代碼?——整潔的代碼只做一件事!糟糕的代碼想做太多事。整潔的代碼力求集中。每個函數、每個類和每個模塊都全神貫注於一事,不受周圍的污染!

a、代碼邏輯簡單、直接(只提供一種而非多種做一件事的途徑),充滿乾淨的抽象和直接的控制語句。代碼應講述事實。——缺陷難以隱藏;

b 、儘量減少依賴關係——便於維護;

c、完善錯誤處理代碼——健壯,如內存泄露、命名方式等;

d、性能最優——速度,被浪費掉的運算週期並不雅觀;

e、有單元測試,使用有意義的命名。明確定義和提供清晰、儘量少的API,代碼應通過其字面表達含義

f、寫小塊的代碼,越小越好! 

g、沒有重複代碼;若同一段代碼反覆出現,就表示某種想法未在代碼中得到良好的體現。就要盡力去找出到底那是什麼,然後再盡力更清晰表達出來。

h、包括儘量少的實體,如類、方法、函數等;檢查對象或方法是否想做的事太多,若對象功能太多,最好是切分爲兩個或多個對象。若方法功能太多,就需要重構,得到一個能較爲清晰說明自身功能的方法,以及另外數個說明如何實現這些功能的方法。

i、有意義的命名是體現表達力的一種方式,往往需要修改好幾次纔會定下名字來。

j、構建簡單抽象:即當發現所有程序都由極爲相似的元素構成時,可以把實現手段封裝到更抽象的方法或類中

k、代碼專爲解決那個問題而存在;每個模塊都爲下一個模塊做好準備。每個模塊都告訴你下一個模塊會是怎樣的。

l、無論是設計系統或單獨的模塊,使用大概可工作的最簡單方案。

 

讀優雅的代碼——令人愉悅;

整潔的代碼總是看起來像是某位特別在意它的人寫的!

 

修改糟糕的代碼,往往越改越爛;

易讀的代碼 != 易修改的代碼

 

讀代碼與寫代碼的時間比例超過10:1。寫新代碼時,一直都在讀舊代碼。不讀周邊代碼就沒法寫代碼。編寫代碼的難度,取決於讀周邊代碼的難度。

 

代碼隨時間流逝而腐壞

 

(2)有意義的命名

取好名字的簡單規則:精確是命名的要點;

a、見名知意

取好名字要花時間。一旦發現有更好的名稱,就換掉舊的。

變量、函數或類的名稱應已經答覆了所有的大問題:爲什麼會存在?它做什麼事?應該怎麼用?若名稱還需要註釋補充,就不算是名副其實!

體現本意的名稱讓人更容易理解和修改代碼!

使用讀得出來的名稱

b、少用前綴

c、做有意義的區分:要區分名稱,就要以讀者能鑑別不同之處的方式來區分

d、專業程序員明確,編寫其他人能理解的代碼;不要讓讀者在腦中把你的名稱翻譯爲他們熟知的名稱

優秀的程序員和設計師,其工作之一就是分離解決方案領域和問題領域的概念。與所涉問題領域更爲貼近的代碼,應當採用源自問題領域的名稱

e、類名:名詞或名詞短語;

f、方法名:動詞或動詞短語;

g、每個抽象概念對應一個詞,並且一以貫之;

h、給名稱添加有意義的語境,但不要添加沒用的語境;只要短名稱足夠清楚,就要比長名稱好。

大多數名稱都不能自我說明的。因此,需要用有良好命名的函數或名稱空間來放置名稱給讀者提供語境。若沒這麼做,給名稱添加前綴是最後一招!

 

當函數內容較長時,可以分解此函數——可以創建一個類語境也增強了,同時算法能夠通過分解爲更小的函數而變得更爲乾淨利落。

 

i、重構時,將名稱改得更好;

 

(3) 函數

函數是所有程序中的第一組代碼;

如何讓函數易於閱讀和理解:保持函數短小、確保只做一件事,讓代碼讀起來像是一系列自頂向下的!

a、第一原則:短小;20行封頂最佳。

b、函數要一目瞭然,每個函數只做一件事(函數只做該函數名下同一抽象層上的步驟)

c、if、else、while語句等,其中的代碼塊應該只有一行,該行是一個函數調用語句;

d、函數的縮進層級不該多於一層或兩層,這樣的函數易於閱讀和理解;

e、編寫函數就是把大一些的概念(即函數的名稱)拆分爲另一抽象層上的一系列步驟

f、每個函數一個抽象層級:基礎概念和細節不要混在一起

g、函數名使用描述性、說明性的名稱:描述函數做的事

若每個程序都讓你感到深合己意,那就是整潔代碼;——爲只做一件事的小函數取個好名字函數越短小、功能越集中,就越便於取個好名字

長而具有描述性的名稱,比短而令人費解的名稱好!

讓函數名稱中的多個單詞容易閱讀,使用這些單詞給函數取個能說清其功用的名稱。 

選擇描述性的名稱能理清你關於模塊的設計思路,並幫你改進之。追索好名稱,往往導致對代碼的改善重構。

命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。

 

h、函數要儘量避免三個參數!最理想的參數數量是0,其次是1,再次是2;從測試的角度看,參數過多,要編寫能確保參數的各種組合運行正常的測試用例很困難。

我們習慣認爲信息通過參數輸入函數,通過返回值從函數中輸出,不太期望信息通過參數輸出應避免使用輸出參數

若僅向函數傳入布爾值,就表示本函數不止做一件事——應把該函數一分爲二

利用一些機制將二元函數轉換爲一元函數;如:可以把writeField(uutputStream, name)方法寫成outputStream的成員之一,從而這樣用:outputStream.writeField(name)。

若函數需要三個或三個以上參數,就說明其中一些參數應該封裝爲類了。也可以使用有可變參數的函數

對於一元函數,函數和參數應當形成一個良好的動詞/名詞對形式

給函數取個好名字,能較好解釋函數的意圖,以及參數的順序和意圖。也可以把參數的名稱寫入到函數名中

 

i、函數應該修改某對象的狀態,或是返回該對象的有關信息,但不要同時做!函數要麼做什麼事,要麼回答什麼事。

 

j、抽離try/catch代碼塊

它搞亂了代碼結構,把錯誤處理和正常流程混爲一談。最好把try和catch代碼塊的主體部分抽離出來,另外形成函數

若try在某個函數中存在,它就該是這個函數的第一個單詞,而且在catch/finally代碼塊後面不該有其它內容。因爲錯誤處理就是一件事。處理錯誤的函數只做一件事。 

使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑代碼中分離出來,得到簡化。如:

if(deletePage(page) == E_OK) {

           if(registry.deleteReference(page.name) == E_OK) {

                    logger.log("aa");

           }else {

                    logger.log("cc");

          }

}else {

          logger.log("bb");

}

可以改爲:

try{

        deletePage(page);

        registry.deleteReference(page.name);

}catch(Exception e) {

        logger.log("aaa");

}

 

返回錯誤碼通常暗示某處有個類或是枚舉,定義了所有錯誤碼。

使用異常代替錯誤碼,新異常可以從異常類派生出來。

 

k、重複是軟件中一切邪惡的根源; 軟件開發領域的所有創新都是在不斷嘗試從源代碼中消滅重複;數據庫範式就是爲了消滅數據重複。

 

l、只要函數保持短小(這個是前提),偶爾出現的return、break或continue語句沒有壞處,甚至比單入單出更具有表達力;goto只在大函數中才有道理,應儘量避免使用。

 

m、switch語句

switch天生做N件事。並且當出現新的條件時,會變得更長。違反了單一職責原則,因爲有好幾個修改它的理由。違反了開閉原則,因爲每當添加新的條件時,就必須修改之。

 解決該問題的方案:將switch語句放到抽象工廠底下,不讓任何人看到。

 

n、函數要無副作用。即承諾只做一件事,但還是會做其他被藏起來的事。因此,一定要在函數名稱中說明

 

如何寫出這樣的函數:

剛開始時,先想什麼就寫什麼,然後再打磨它。初稿也許粗陋無序,通過斟酌推敲,直到遵循以上列出的規則。

剛寫函數,都冗長而複雜,然後分解函數、修改名稱、消除重複,縮短和重新安置方法,拆散類等,但要保證單元測試通過,遵循以上列出的規則。

 

每個系統都是使用某種領域特定語言搭建,這種語言是程序員設計來描述那個系統的。

領域特定語言的一個部分,就是描述在系統中發生的各種行爲的函數層級

 

真正的目標在於把系統當做故事來講,而不是當做程序來寫。你編寫的函數必須乾淨利落地拼裝到一起,形成一種精確而清晰的語言,幫助你講故事!

 

(4)註釋

若編程語言足夠有表達力,或者我們長於用這些語言來表達意圖,就不那麼需要註釋。

註釋是彌補我們在用代碼表達意圖時遭遇的失敗

註釋存在的時間越久,就離其所描述的代碼越遠。

 

只有代碼能忠實告訴你它做的事,那是唯一真正準確的信息來源。

用代碼來解釋。只需要創建一個描述與註釋所言同一事物的函數即可。

 

對於註釋的代碼,因爲有源代碼控制系統能記住這些不要的代碼,因此直接刪掉即可,無需註釋。

 

註釋的作用是解釋未能自行解釋的代碼。

 

短函數不需要太多描述爲只做一件事的短函數選個好名字比寫註釋要好

 

(5)格式(包括直垂方向和水平方向

短文件比長文件易於理解。一個文件最多500行。 

 

源文件名稱本身應該足夠告訴我們是否在正確的模塊中。源文件最頂部應該給出高層次概念和算法。細節應該往下漸次展開,直至找到源文件中最底層的函數和細節。

 

每個空白行都是一條線索,標識出新的獨立概念

關係密切的概念應該相互靠近。如變量聲明應儘可能靠近其使用位置;成員變量放在類的頂部聲明;若某個函數調用了另外一個,就應該將其放在一起,且調用者應放在被調用者上面;

概念相關的代碼應該放在一起。

自上而下展示函數調用順序,建立自頂向下貫穿源代碼模塊的良好信息流。 最重要的概念先出來,以最少的細節表述它們,底層細節最後出來。

 

短代碼行——一行最多120個字符

用不對齊的聲明和賦值。

若類中屬性的聲明的列表長度過長,就表示該類應該被拆分了

縮進:表示層級。

 

在團隊中工作就是團隊說了算。軟件要有一以貫之的風格。

軟件系統由一系列代碼文件組成。

 

(6)對象和數據結構

不願暴露數據細節,更願意以抽象形態表述數據要以更好的方式呈現某個對象包含的數據

 

對象:把數據隱藏在抽象之後,暴露操作數據的函數。

數據結構:暴露其數據,沒有提供有意義的函數。

 

當需要添加新數據類型而不是新函數時,此時對象和麪向對象比較適合;當需要添加新函數而不是數據類型時,此時過程式代碼和數據結構更合適。

 

連串的調用被認爲是骯髒的代碼,最好做切分!

 

模塊不應瞭解它所操作對象的內部情形。

DTO(數據傳輸對象):在一系列將原始數據轉換爲數據庫的過程中常使用。不要在這樣的數據結構中添加業務方法

 

(7)錯誤處理(遇到錯誤時,最好拋出一個異常調用代碼很整潔,其邏輯不會被錯誤處理搞亂

錯誤處理是編程時必須要做的事之一。因爲輸入可能出現異常,可能會出錯。當錯誤發生時,程序員有責任確保代碼照常工作

 

錯誤處理很重要,若它搞亂了代碼邏輯,就是錯誤的做法。

 

處理錯誤代碼的技巧:

a、使用異常而非返回碼。遇到錯誤時,最好拋出一個異常調用代碼很整潔,其邏輯不會被錯誤處理搞亂

b、先寫try-catch-finally語句。無論try代碼塊中執行的代碼出什麼錯,能幫你定義代碼的用戶應該期待什麼。

c、在catch中,要記錄日誌。

d、依調用者需要定義異常類。在應用程序中定義異常類時,最重要的考慮應該是它們如何被捕獲。

將第三方API打包是一個很好的最佳實踐。

e、區隔業務邏輯和錯誤處理代碼,會變得整潔!——還可以寫得更簡潔,即創建一個類或配置一個對象,用來處理特例。你來處理特例,客戶代碼就不用應付異常行爲了。異常行爲被封裝到特例對象中。

f、別返回null值。——避免NullPointerException的出現,代碼就更整潔!

若打算在方法中返回null值,不如拋出異常,或是返回特例對象。若在調用某個第三方API中可能返回null值的方法,可以考慮用新方法打包這個方法,在新方法中拋出異常或返回特例對象。

返回null值就是在給自己增加工作量。只要有一處沒檢查null值,應用程序就會失控

java有Collections.emptyList()方法,該方法返回一個預定義不可變列表。

g、別傳遞null值:禁止傳入null值。

除非API要求你向它傳遞null值,否則就要儘可能避免傳遞null值。

定義了異常,就得定義處理器!

傳入null值,會得到運行時錯誤。在大多數編程語言中,沒有良好的方法能對付由調用者意外傳入的null值。恰當的做法就是禁止傳入null值。這樣,編碼時傳入null值就意味着出問題了。

 

整潔代碼是可讀的,但也要強固。 若將錯誤處理隔離看待,獨立於主要邏輯之外,就能寫出強固而整潔的代碼。

 

(8)邊界?

我們都需要將外來代碼乾淨地整合進自己的代碼中。 

 

8.2節沒有閱讀完!

 

保持軟件邊界整潔的方法:

a、編寫測試是學習API最好途徑。需要對第三方程序包寫單元測試,瞭解其API的行爲是否發生了改變。

可以使用這些邊界測試來減輕遷移的勞力。

b、建議不要將Map(或在邊界上的其他接口)在系統中傳遞。若要使用類似Map這樣的邊界接口,就把它保留在類中。因爲Map需要進行類型轉換,不是整潔的代碼。

避免從公共API中返回邊界接口,或將邊界接口作爲參數傳遞給公共API。

 

良好的軟件設計,無需巨大投入和重寫即可進行修改。在使用我們控制不了的代碼時,要確保未來的修改不至於代價太大。

邊界上的代碼需要清晰的分割和定義了期望的測試。可以對第三方接口進行包裝,或者使用adapter模式

 

(9)單元測試

測試代碼和生產代碼一樣重要。它需要被思考、被設計,它該像生產代碼一樣保持整潔

 

覆蓋了生產代碼的自動化單元測試程序能儘可能保持設計和架構的整潔

測試使得改動變爲可能

丟失了測試,代碼也開始腐壞!

 

整潔的測試:可讀性——明確、簡潔、足夠的表達力。在測試中,要以儘可能少的文字表達大量內容。

 

每個測試都可以清晰的拆分爲三個環節:

第一個環節是構造測試數據,第二個環節是操作測試數據,第三個部分是檢驗操作是否得到期望的結果。

 

將細節作封裝。讀測試的人一讀就能搞清楚狀況,不至於被細節誤導或嚇到

 

每個測試一個斷言!

每個測試一個概念,即只做一件事!

 

對於重複代碼——可以使用模板模式,將不變的放到基類中,將變的放到派生類中!

 

整潔的測試遵循以下5個原則:

a、快速。測試應該夠快。

b、獨立。測試應該相互獨立。

c、可重複

d、自我驗證。測試應該有布爾值輸出

e、及時。測試應及時編寫。單元測試應該恰好在使其通過的生產代碼之前編寫

 

測試保證和增強了生產代碼的可擴展性、可維護性和可複用性。因此,要保持測試代碼整潔,讓測試具有表達力並短小精悍

 

發明作爲面向特定語言的測試API,幫助自己編寫測試!

 

如果你坐視測試腐壞,那麼代碼也會跟着腐壞

 

(10)

代碼語句以及由代碼語句構成的函數的表達力

 

a、首先會想實質保有隱私,放鬆封裝總是下策。

b、類應該短小精悍衡量的方法爲職責

類的名稱應當描述其職責。命名是幫助判斷類的長度的第一個手段,若無法爲某個類命以精確的名稱,這個類就太長了。

c、類或模塊應有且有一條加以修改的理由——單一職責原則。

鑑別職責可以幫助我們在代碼中認識到並創建出更好的抽象。

代碼組織和整潔

每個達到一定規模的系統都會包括大量邏輯和複雜性。管理這種複雜性的首要目標是加以組織,以便開發者知道在哪兒能找到東西。

系統應該由許多短小的類而不是少量巨大的類組成。每個小類封裝一個職責,只有一個修改的原因。

d、內聚

類應該只有少量成員變量。若一個類中的每個成員變量都被每個方法所使用,則該類具有最大的內聚性。一般來說,這是不可能的。但是是追求的目標

內聚性高,意味着類中的方法和變量相互依賴、互相結合成一個邏輯整體。

 

保持函數和參數列表短小,有時會導致一組子集方法所用的成員變量數量增加。此時意味着至少有一個類要從大類中分拆,應嘗試將這些變量和方法分拆到兩個或多個類中,讓新的類更爲內聚。

e、保持內聚性就會得到許多短小的類

堆積了越來越多隻爲允許少量函數共享而存在的成員變量——>類喪失了內聚性。當類喪失了內聚性,就拆分它若有些函數想要共享某些變量,就讓它們擁有自己的類

將大函數拆分爲小函數,往往也是將類拆分爲多個小類的時機。程序會更加有組織,也會擁有更爲透明的結構。

 

f、爲了修改而組織

修改一直持續。

每次修改都讓我們冒着系統其它部分不能如期望般工作的風險。因此,對類加以組織,以降低修改的風險

 

出現了只與類的一小部分有關的私有方法行爲,意味着存在改進空間。

打開類,風險也隨之而來

對類的任何修改(包括打開類)都有可能破壞類中的其它代碼。 但對類進行擴展(也是有修改),就不會破壞類中的其它代碼。——多態

一旦打開了類,就應當修正設計方案

私有方法,放到需要它的類中。

公共的方法,放到獨立的工具類中。

 

g、隔離修改:通過接口和抽象類來實現。

抽象類只呈現概念。

 

面向接口編程——>系統解耦,會更加靈活、更加可複用。部件之間的解耦代表着系統中的元素相互隔離得很好。隔離讓對系統每個元素的理解更容易。

 

依賴倒置原則:本質就是應當依賴於抽象而不是依賴於具體的實現。

 

(11)系統

 有些人負責全局,其它人負責細節

恰當的抽象等級和模塊,能讓組件在不瞭解全局時也能有效運轉。

 

在較高的抽象層級(系統層級)上保持整潔。

 

a、將系統的構造與使用分開

軟件系統應將啓始過程和啓始過程之後的運行時邏輯分離開

每個應用程序都該留意啓始過程。將關注的方面分離開,是很重要的設計技巧

缺乏模塊組織性,通常會有許多重複代碼。

 

main函數創建系統所需的對象,再傳遞給應用程序,應用程序只管使用。

有時應用程序也要負責確定何時創建對象,此時可用抽象工廠模式讓應用自行控制何時創建實體,構造的細節隔離於應用程序代碼之外

依賴注入可以實現分離構造和使用。這種授權機制通常要麼是main例程,要麼是有特定目的的容器。如JNDI查找就是依賴注入的一種部分實現。在JNDI中,對象請求目錄服務器提供一種符合某個特定名稱的“服務”。

 

延後初始化:多數DI容器在需要對象之前並不構造對象,這類容器提供調用工廠或構造代理的機制。

 

b、擴容

一開始就做對系統是神話。我們應該只去實現今天的用戶故事,然後重構,明天再擴展系統、實現新的用戶故事。這就是迭代和增量敏捷的精髓。測試驅動開發、重構以及它們打造出的整潔代碼,在代碼層面保證了這個過程的實現。

 

軟件系統的架構可以遞增式增長,只要我們持續將關注面恰當切分

 

AOP是一種恢復橫貫式關注面模塊化的普適手段。

java代理適用於簡單的情況,如在單獨的對象或類中包裝方法調用。JDK提供的動態代理僅能與接口協同工作。對於代理類,得使用字節碼操作庫,如CGLIB、ASM或Javassist。

Proxy API需要一個InvocationHandler對象,用來實現對代理的全部接口的方法調用

 

使用描述性配置文件或API,把需要的應用程序構架組合起來,包括持久化、事務、安全、緩存、恢復等橫貫性問題。——實際情況是框架以對用戶透明的方式處理使用java代理或字節碼庫的機制。

 

通過方面來實現關注面切分的功能最全的工具是AspectJ語言。它將方面作爲模塊構造處理支持Java擴展。

spring AOP和JBoss AOP提供純java實現手段。

 

AspectJ引入了使用Java5 annotation定義純java代碼的方面。

 

c、沒必要先做設計。將架構按需從簡單演化到精細。

 

只要軟件的構架有效切分了各個關注面,還是有可能做根本性改動的。可以從簡單自然但切分良好的架構開始做軟件項目,隨着規模的增長添加更多基礎架構。

網站採用了精密的數據緩存、安全、虛擬化等技術,獲得了極高的可用性和性能。在每個抽象層和範圍之內,那些最小化耦合的設計都簡單到位,效率和靈活性也隨之而來。

 

對總的覆蓋範圍、目標、項目進度和最終系統的總體架構,要有所預期。必須有能力隨機應變。

 

最佳的系統架構是由模塊化的關注面領域組成,每個關注面均用純java對象實現。

 

d、模塊化和關注面切分成就了分散化管理和決策

 

e、系統需要領域特定語言

領域特定語言是一種單獨的小型腳步語言或以標準語言寫就的API,領域專家用它編寫代碼。

 

優秀的領域特定語言填平了領域概念和實現領域概念的代碼之間的鴻溝。

敏捷實踐優化了開發團隊和甲方之間的溝通。

若用和領域專家使用的同一種語言來實現領域邏輯,就會降低不正確將領域翻譯爲實現的風險。

 

領域特定語言在有效使用時,能提升代碼和設計模式之上的抽象層次。

領域特定語言允許所有抽象層級和應用程序中的所有領域,從高級策略到底層細節,使用POJO來表達。

  

系統應該是整潔的。侵害性架構會湮滅領域邏輯。當領域邏輯受到困擾,質量就會堪憂。

 

在所有的抽象層級上,意圖都應該清晰可辨

 

無論是設計系統或單獨的模塊,使用大概可工作的最簡單方案。

 

(12)迭

通過跌進設計達到整潔目的。

 

簡單設計的4條規則:(以下規則按其重要程度排列)

a、運行所有測試;

b、不可重複

c、表達了程序員的意圖

d、儘可能減少類和方法的數量優先級最低

 

只要系統可測試,就會導向保持類短小且目的單一的設計方案。

確保系統完全可測試能幫助我們更好的設計

緊耦合的代碼難以編寫測試。編寫測試越多,就越會遵循DIP之類的規則。

使用依賴注入、接口和抽象儘可能減少耦合,如此設計就會進步。

 

遞增式地重構代碼

測試消除了對清理代碼就會破壞代碼的恐懼。

 

在重構過程中,提升內聚性,降低耦合度,切分關注面,模塊化系統性關注面,縮小函數和類的尺寸,選用更好的名稱等。

 

重複是擁有良好設計系統的大敵。

重複有多種表現,極其雷同的代碼行也是重複類似的代碼往往可以調整得更相似

重複也有實現上的重複。 

 

要創建整潔的系統,需要有消除重複的意願。即便對於短短几行也是如此。

 

軟件項目的主要成本在於長期維護。

爲了在修改時儘量降低出現bug的可能性,要理解系統是做什麼。

代碼要清晰表達作者的意圖。

短小的類和函數易於命名,易於理解。

通過在實現模式的類的名稱中採用標準模式名,能充分向其他開發者描述你的設計。

測試的主要目的之一是通過實例起到文檔的作用,讀到測試的人能很快理解某個類是做什麼的

 

做到有表達力的最重要方式是嘗試。——花一點時間在每個函數和類上。

 

(13)併發編程

編寫整潔的併發程序非常難。編寫在單線程中執行的代碼簡單得多。系統一旦遭受壓力,代碼就扛不住了。

 

併發是一種解耦策略,它幫助我們把做什麼(目的)和何時(時機)做分解開。

解耦目的與時機能明顯改進應用程序的吞吐量和結構。從結構的角度看,應用程序更像是許多臺協同工作的計算機。

 

要了解容器在做什麼。瞭解如何對付併發更新、死鎖等問題。

 

編寫併發軟件:

a、併發會在性能和編寫額外代碼上增加一些開銷;

b、正確的併發是複雜的

c、併發缺陷並非總能重現

d、併發常常需要對設計策略的根本性修改。 

 

兩個線程會相互影響。因爲線程在執行一行代碼時有許多可能路徑。

有多少種不同路徑?——需要理解just-in-time編譯器如何對待生成的字節碼,還要理解java內存模型認爲什麼東西具有原子性。

 

併發防禦原則:

a、單一職責原則(SRP)認爲,方法、類、組件應當只有一個修改的理由。

建議分離併發代碼和其他代碼

b、限制數據作用域。

謹記數據封裝;嚴格限制對可能被共享的數據的訪問

c、使用數據複本

d、線程應儘可能獨立。

讓每個線程在自己的世界中存在,不與其他線程共享數據。每個線程處理一個客戶端請求,從不共享的源頭接納所有請求數據,存儲爲本地變量。這樣每個線程都像是世界中的唯一線程,沒有同步需要。

嘗試將數據分解到可被獨立線程(可能在不同處理器上)操作的獨立子集。

 

在用java5編寫線程代碼,要注意以下幾點:

a、使用類庫提供的線程安全羣集;如java.util.concurrent包,該代碼包中的羣集對於多線程解決方案是安全的,執行良好。

b、使用executor框架執行無關任務;

c、儘可能使用非鎖定解決方案;

d、有幾個類並不是線程安全的;

 

還有幾個支持高級併發設計的類,如ReentrantLock類,可在一個方法中獲取,在另一個方法中釋放的鎖;Semaphore類,經典的‘信號’的一種實現,有計數器的鎖;CountDownLatch類,在釋放所有等待的線程之前,等待指定數量事件發生的鎖。這樣,所有線程都平等的幾乎同時啓動。

 

對於Java,掌握java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。

 

限定資源:併發環境中有着固定尺寸或數量的資源。如數據庫連接和固定尺寸讀、寫緩存等;

互斥:每一時刻僅有一個線程能訪問共享數據或共享資源;

線程飢餓:一個或一組線程在很長時間內或永久被禁止。如,總是讓執行快的線程先運行,假如執行快的線程沒完沒了,則執行時間長得線程就會捱餓。

死鎖:兩個或多個線程互相等待執行結束。每個線程都擁有其他線程需要的資源,得不到其他線程擁有的資源,就無法終止。

活鎖:執行次序一致的線程,每個都想要起步,但發現其它線程已經在路上。由於競步的原因,線程會持續嘗試起步,但在很長時間內卻無法如願,甚至永遠無法啓動。

 

在併發編程中用到的幾種執行模型:

a、生產者-消費者模型

一個或多個生產者線程創建某些工作,並置於緩存或隊列中。一個或多個消費者線程從隊列中獲取並完成這些工作。生產者和消費者之間的隊列是一種限定資源。

b、讀者-作者模型?

當存在一個主要爲讀者線程提供信息源,但只偶爾被作者線程更新的共享資源,吞吐量就會是個問題。增加吞吐量,會導致線程飢餓和過時信息的累積。更新會影響吞吐量。協調讀者線程,不去讀作者線程正在更新的信息(反之亦然),是一種辛苦的平衡工作。作者線程傾向於長期鎖定許多讀者線程,從而導致吞吐量問題。

挑戰之處在於平衡讀者線程和作者線程的需求,實現正確操作,提供合理的吞吐量,避免線程飢餓。

c、 宴席哲學家

用線程代替哲學家,用資源代替叉子,就變成了進程競爭資源的情形。若沒有用心設計,這種競爭式系統會遭遇死鎖、活鎖、吞吐量和效率降低等問題。

 

可能遇到的併發問題,大多數都是這三個問題的變種。請研究並使用這些算法,這樣,遇到併發問題時你就能有解決問題的準備了。

 

警惕同步方法之間的依賴:避免使用一個共享對象的多個方法

若必須要使用一個共享對象的多個方法,在這種情況發生時,有3種寫對代碼的手段:

1、基於客戶端的鎖定——客戶端代碼在調用第一個方法前鎖定服務端,確保鎖的範圍覆蓋了調用最後一個方法的代碼; 

2、基於服務端的鎖定——在服務端創建鎖定服務端的方法,調用所有方法,然後解鎖。讓客戶端代碼調用新方法。

3、適配服務端——創建執行鎖定的中間層。這是一種基於服務端的鎖定的例子,但不修改原始服務端代碼。

 

保持同步區域微小

synchronized製造了鎖。同一個鎖維護的所有代碼區域在任一時刻保證只有一個線程執行。鎖是昂貴的,因爲它帶來了延遲和額外開銷。

 

很難編寫正確的關閉代碼:

建議儘早考慮關閉問題,儘早令其工作正常。這會花費比你預期更多的時間。

 

將僞失敗看作可能的線程問題;

先使非線程代碼可工作;偶發事件被忽略得越久,代碼就越有可能搭建於不完善的基礎之上。

編寫可插拔的線程代碼;這意味着創建由線程調用的POJO。能放進POJO中的代碼越多越好。

編寫可調整的線程代碼;

運行多於處理器數量的線程;這是爲了促使任務交換的發生。因爲系統在切換任務時會發生一些事。任務交換越頻繁,越有可能找到錯過臨界區或導致死鎖的代碼。

在不同平臺上運行;不同操作系統有着不同線程策略,不同的線程策略影響了代碼的執行。在不同環境中,多線程代碼的行爲也不一樣。儘早並經常地在所有目標平臺上運行線程代碼。

調整代碼並強迫錯誤發生;

 

裝置調試代碼:增加對Object.wait()、Object.sleep()、Object.yield()和Object.priority()等方法的調用,改變代碼執行順序。因爲這些方法都會影響執行順序,從而增加了檢查到bug的可能性。

有兩種裝置代碼的方法:

硬編碼:即手工向代碼中加入。若將系統分解爲對線程及控制線程的類一無所知的POJO,就能更容易找到裝置代碼的位置。

自動化:使用CGLIB或ASM等之類工具通過編程來裝置代碼。

要點是讓代碼‘異動’,從而使線程以不同次序執行。編寫良好的測試與異動相結合,能有效增加發現bug的機會

 

併發代碼很難寫正確。加入多線程和共享數據後,簡單的代碼也會出錯。要編寫併發代碼,就得嚴格編寫整潔的代碼。

第一要訣就是遵循單一職責原則。將系統切分爲分離了線程相關代碼和線程無關代碼的POJO。確保在測試線程相關代碼時只是在測試,沒有做其他事情。線程相關代碼應保持短小和目的集中

 

瞭解併發問題的可能原因,對共享數據的多線程操作,或使用了公共資源池。類似平靜關閉或停止循環之類邊界情況尤其棘手。

學習類庫,瞭解基本算法。理解類庫提供的與基礎算法類似的解決問題的特性。

學習如何找到必須鎖定的代碼區域並鎖定之。不要鎖定不必鎖定的代碼。避免從鎖定區域中調用其他鎖定區域。

花點時間裝置代碼,就能極大提升發現bug的機會

 

(14)逐步改進

編程是一種技能。

要編寫整潔代碼,必須先寫骯髒的代碼,然後再清理它

 

若希望代碼結構一直可維護,就需要調整了。

許多種不同類型,類似的方法——>類。

毀壞程序最好的方法之一就是以改進之名大動其結構

採用TDD是爲了保持系統始終能運行

每次修改一個地方,持續運行測試。若測試出錯,在做下一個修改前確保通過

 

一個類最好就拋出唯一一個異常。如Args類就拋出一個ArgsException。並且將大量的錯誤支持代碼從Args類轉移到ArgsException類中。

 

重構就是一種不停試錯的迭代過程。

模塊都能再改進,每個人都有責任把模塊改進得比發現時更整潔

 

(15)

a、首先,讓它能工作;

b、讓它做對;

 

用Clover檢查單元測試覆蓋了哪些代碼。

 

serialVersionUID變量用於控制序列號——自動控制序列號

 

使用解釋臨時變量模式使函數中的算法更爲透明;

將抽象方法移到頂層類中;

 

 若兩個方法中存在一些重複,可以抽離一個新方法來消除重複

 

每次修改都要問自己爲什麼要這樣改,把修改的原因寫下來

 

(16)總結

註釋:只應該描述有關代碼和設計的技術性信息;註釋會很快過時,若發現廢棄的註釋,最好儘快更新或刪除;註釋應提及代碼自身沒提到的東西

看到註釋掉的代碼,就刪除它!因爲源代碼控制系統會記得它。

 

應當能夠使用單個命令簽出系統,並用單個指令構建它。

應當能夠發出單個指令就可以運行全部單元測試。

 

函數的參數量應該少;

布爾值參數宣告了函數不止做一件事,應消滅掉;

永不被調用的方法應該刪除;

 

盡力減少源文件中額外語言的數量和範圍;

代碼應該有正確行爲——開發者常常寫出他們以爲能工作的函數,而不是去證明代碼在所有的角落和邊界情形下真能工作追索每種邊界條件,並編寫測試

 

每次看到重複代碼,都代表遺漏了抽象。重複的代碼可能成爲子程序或乾脆是另一個類。將重複代碼疊放進類似的抽象,增加了你的設計語言的詞彙量,提升了抽象層級。

重複最明顯的形態就是不斷看到明顯一樣的代碼,不斷在複製粘貼代碼,可以用單一方法來替代之

較隱蔽的形態是在不同模塊中不斷重複出現、檢測同一組條件的switch/case或if/else鏈。可以用多態來替代之。

更隱蔽的形態是採用類似算法但具體代碼行不同的模塊,這也是重複,可以使用模板方法模式或策略模式來修正。

 

設計模式是消除重複的有效手段。 範式是消除數據庫設計中的重複的策略。oo自身也是組織模塊和消除重複的策略。

 

創建分離較高層級一般性概念與較低層級細節概念的抽象模型,分離要完整,這很重要。通過創建抽象類來容納較高層級概念,創建派生類來容納較低層次概念。

與細節實現有關的常量、變量或工具函數不應該在基類中出現。

良好的軟件設計要求分離位於不同層級的概念。 

 

死代碼就是不執行的代碼。可以在檢查不會發生的條件的if語句體中找到。可以在從不拋出異常的try語句的catch塊中找到。可以在永不會發生的switch/case條件中找到。

找到死代碼,就要刪除它。

 

變量和函數應該在靠近被使用的地方定義。

 

取名字要前後一致,這樣讓代碼更加易讀。

 

保持源文件整潔,良好的組織,不被搞亂。刪除不用的

 

類的方法只應對其所屬類中的變量和函數感興趣,不該垂青其它類中的變量和函數。(類總持有另一個對象的引用屬於例外) 

代碼要儘可能具有表達力

 

(17)


 

2、TIPS

(1)什麼是好的代碼?如何提高代碼質量?如何通過代碼評審,提高設計能力?

a、運行起來實現功能;

b、要應對變化,易擴展(對修改關閉,對擴展打開!)

c、易讀、可維護性。

d、性能

 

(2)測試

只要還有沒被測試探測過的條件,或是有沒被驗證的,測試就還不夠。

 

使用覆蓋率工具。覆蓋率工具能彙報你測試策略中的缺口。

 

測試邊界條件。算法的中間部分正確但邊界判斷錯誤的情形很常見

 

bug趨向於扎堆。在某個函數中發現一個bug時,最好全面測試那個函數,可能會發現bug不止一個。

 

測試應該快速。

 

(3) 

 

(4)

 

(5)

 

(6)自頂向下讀代碼:向下規則

每個函數後面都跟着位於下一抽象層級的函數。

 

(7)違反單一職責原則:因爲不只做了一件事,有好幾個修改它的理由;

違反開閉原則:因爲每當添加新類型時,就必須修改之;

 

(8)21種代碼壞味道

a、重複的代碼

b、過長的函數

c、過大的類

d、過長的參數列表

e、發散式變化

f、分散的修改

g、僞面向對象的調用

h、數據泥團

i、基本類型的誤用

j、switch-case結構的誤用

k、平行繼承體系

l、過薄的類

m、只有局部意義的成員變量

n、過度耦合的消息鏈

o、過薄的中間對象

p、緊耦合類

q、相似的類

r、只有數據的類

s、濫用類的繼承關係


(3)代碼命名

類名:通過名字表示;描述功能;描述存儲的對象;

 

(4)

 

(5)


(6)


3、PS

(1)一個大訪問量和高負載的網站,性能好壞取決於架構,而不取決於編程語言!

 

(2)軟件生命週期:設計、編碼、重構、單元測試、持續集成等。

重構是實現優秀設計的一種重要手段。

 

(3)代碼靜態分析工具:方便在編碼階段就能找出可能的編碼缺陷,如java中的findbugs、checkstyle、PMD、javancss等。

checkstyle:用於編碼標準;

PMD的CPD:幫助發現代碼質量;

coverlipse:測量代碼覆蓋率;

JDepend:提供依賴項分析;

metric:有效查出複雜度;

 

代碼分析工具:Similarity Analyzer、Jester;

 

(4)軟件應對需求變化的能力越來越差——>架構重構。

 

(5)建議將算法分成多個函數!

 

(6)維護別人寫的代碼,使用重構工具解決問題,效果好!

 

(7)類的結構層次分明!

 

(8)code需要經歷的考驗:

a、單元測試

b、冒煙測試

c、build verification test

d、系統測試

e、故障演習

f、測試機羣

g、生產機羣

 

(9)怎樣review別人的代碼?

需要從各個截面去理解。

用戶接口:

出錯代碼:

內部狀態:這些狀態如如何相互轉換的;

測試用例:每種轉換的情況是否有case?性能如何?

某個方法:變量的使用;檢查多線程問題;檢查邏輯錯誤


保證可讀性:

若3分鐘之內讀不懂,讓被review的人修改;

 

(10)合適的地方打合適級別的log

五級:debug、info、warn、error、fatal。

系統內在的一些邏輯(如每個函數的進入、退出):debug;

正常的邏輯(如正常的執行了用戶的一個請求):info;

非正常的邏輯(如用戶試圖刪除不存在的文件):warn;

系統錯誤(如發現有一塊磁盤壞了):error;

不可恢復的錯誤:fatal;

 

(11)

 



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