代碼整潔之道【筆記】
一、整潔代碼
A.混亂的代價1.有些團隊在項目初期進展迅速,但有那麼一兩年的時間卻慢去蝸行。對代碼的每次修改都影響到其他兩三處代碼2.花時間保持代碼整潔不但有關效率,還有關生存3.程序員遵從不了解混亂風險經理的意願,也是不專業的做法4.Bjarne Stroustrup,C++發明者:我喜歡優雅和高效的代碼。代碼邏輯應該直接了當,叫缺陷難以隱藏;盡量減少依賴關係,使之便於維護;依據某種分層戰略完善錯誤處理代碼;性能調至最優,省得引誘別人做沒規矩的優化,搞出一堆混亂來。整潔的代碼只做好一件事。
5.Grady Booch,《面向分析與設計》:整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了乾淨利落的抽象和直接了當的控制語句。6.Dave Thomas,OTI公司創始人:整潔的代碼應可由作者之外的開發者閱讀和增補。它應有單元測試和驗收測試。它使用有意義的命名。它只提供一種而非多種做一件事的途徑。它只有盡量少的依賴關係,而且要明確地定義和提供清晰、盡量少的API。代碼應通過其字面表達含義,因為不同的語言導致並非所有必須信息均可通過代碼自身清晰表達。7.Michael Feathers,《修改代碼的藝術》:我可以列出我留意到的整潔代碼的所有特點,但其中有一條是根本性的。整潔的代碼總是看起來像是某位特別在意它的人寫的。幾乎沒有改進的餘地。代碼作者什麼都想到了,如果你企圖改進它,總會回到原點,讚歎某人留給你的代碼——全心投入的某人留下的代碼。8.Ron Jeffries,《極限編程實施》:簡單代碼,依其重要順序:能通過所有測試;沒有重複代碼;體現系統中的全部設計理念;包括盡量少的實體,比如類、方法、函數等9.Ward Cunningham,Wiki發明者:如果每個常式都讓你感到深合已意,那就是整潔代碼。如果代碼讓編程語言看起來像是專為解決那個問題而存在,就可以稱之為漂亮的代碼。B.思想流派
1.讀與寫花費時間的比例起過10:1C.童子軍軍規
1.「讓營地比你來時更乾淨」2.如果每次簽入時,代碼都比簽出時乾淨,那麼代碼就不會腐壞
二、有意義的命名
A.名副其實1.變數、函數或類的名稱應該已經答覆了所有的大問題,如果名稱需要注釋來補充,那就不算名副其實2.代碼的模糊度:即上下文在代碼中未被明確體現的程度B.避免誤導
1.程序員必須避免留下掩藏代碼本意的錯誤線索。應當避免使用與本意相悖的詞2.以同樣的方式拼寫出同樣的概念才是信息,拼寫前後不一致就是誤導3.要注意使用小寫字母i和大寫字母O作為變數名,看起來像「壹」和「零」C.做有意義的區分
1.同一作用範圍內兩樣不同的東西不能重名,如果名稱必須相異,那其意思也應該不同才對
2.廢話是另一種沒意義的區分。假設你有一個Product類,如果還有一個ProductInfo或ProductData類,那它們的名稱雖然不同,意思卻無區別3.只要體現出有意義的區分,使用a和the這樣的前綴就沒錯4.廢話都是冗餘。Variable一詞記錄不應當出現在變數名中,Table一詞永遠不應當出現在表名中D.使用讀得出來的名稱
E.使用可搜索的名稱1.單字母名稱和數字常量有個問題,就是很難在一大篇文字中找出來F.避免使用編碼
1.把類型或作用域編進名稱裡面,徒然增加了解碼的負擔2.也不必用m_前綴來標明成員變數,應當把類和函數做得足夠小,消除對成員前綴的需要3.不加修飾的介面,不要用前導字母I
G.避免思維映射
1.不應當讓讀者在腦中把你的名稱翻譯為他們熟知的名稱,單字母變數名就是個問題2.專業程序員了解,明確是王道H.類名
1.類名和對象名應該是名詞或名詞短語,類名不應當是動詞I.方法名
1.方法名應該是動詞或動詞短語。屬性訪問器、修改器和斷言應該根據其值命名,並依Javabean標準加上get、set和is前綴2.可以考慮將相應構造器設置為private,強制使用這種命名手段J.別扮可愛
1.言到意到,意到言到
K.別用雙關語
1.避免將同一單詞用於不同目的2.應儘力寫出易於理解的代碼,把代碼寫得讓別人能一目盡覽而不必殫精竭慮地研究L.使用解決方案領域名稱
1.儘管用那些計算機科學術語、演算法名、模式名、數學術語M.使用源自所涉問題領域的名稱
1.如果不能用程序員熟悉的術語來給手頭的工作命名,就採用從所涉問題領域而來的名稱2.優秀的程序員和設計師,其工作之一就是分離解決方案領域和問題領域的概念N.添加有意義的語境
1.你需要用有良好命名的類、函數或名稱空間來放置名稱,給讀者提供語境
2.如果沒這麼做,給名稱添加前綴就是最後一招了O.不要添加沒用的語境
1.只要短名稱足夠清楚,就要比長名稱好P.最後的話
1.取好名字最難的地方在於需要良好的描述技巧和共有文化背景三、函數
A.短小1.函數的第一規則是要短小,第二條規則是還要更短小2.if語句、else語句、while語句等,其中的代碼塊應該只有一行,該行大抵是一個函數調用語句3.函數不應該大到足以容納嵌套結構,所以,函數的縮進層級不該多於一層或兩層
B.只做一件事
1.函數應該做一件事。做好這件事,只做這一件事2.要判斷函數是否不止做了一件事,就是看看是否能再拆出一個函數,該函數不僅只是單純地重新詮釋其實現3.只做一件事的函數無法被合理地切分為多個區段C.每個函數一個抽象層級
1.要確保函數只做一件事,函數中的語句都要在同一抽象層級上2.自頂向下讀代碼:向下規則,讓代碼擁有自頂向下的閱讀順序,讓每個函數後面都跟著下一抽象層級的函數D.switch語句
1.寫出短小的switch語句很維,寫出只做一件事的switch語句也很難,Switch天生要做N件事2.將switch語句埋到抽象工廠底下,不讓任何人看到
3.如果只出現一次,用於創建多態對象,而且隱藏在某個繼承關係中,在系統其他部分看不到,就還能容忍E.使用描述性的名稱
1.沃德原則:「如果每個常式都讓你感到深合已意,那就是整潔代碼」2.函數越短小,功能越集中,就越便於取個好名字3.別害怕長名稱,長而具有描述性的名稱,要比短而令人費解的名稱好4.命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名F.函數參數
1.最理想的參數數量是零,有足夠的理由才能用三個以上參數2.事件:在這種形式中,有輸入參數而無輸出參數,程序將函數看作一個事件,使用該參數修改系統狀態3.對於轉換,使用輸出參數而非返回值令人迷惑,如果函數要對輸入參數進行轉換操作,轉換結果就該體現為返回值
4.向函數傳入布爾值會使方法簽名立刻變得複雜起來,大聲宣布函數不止做一件事5.如果函數看來需要兩個、三個或三個以上參數,就說明其中一些參數應該封裝為類了6.有可變參數的函數可能是一元、二元甚至三元,超過這個數量就可能要犯錯了7.對於一元函數,函數和參數應當形成一種非常良好的動詞/名詞對形式G.無副作用
1.函數承諾只做一件事,但還是會做其他被藏起來的事,會導致古怪的時序性耦合及順序依賴2.參數多數會被自然而希地看作是函數的輸入H.分隔指令與詢問
1.函數要麼做什麼事,要麼回答什麼事,但二者不可得兼I.使用非同步替代返回錯誤碼
1.從指令式函數返回錯誤碼輕微違反了指令與詢問分隔的規則。它鼓勵了在if語句判斷中把指令當作表達式使用2.try/catch代碼塊把錯誤處理與正常流程混為一談,最好把try和catch代碼塊的主體部分抽離出來,另外形成函數3.錯誤處理就是一件事,處理錯誤的函數不該做其他事4.依賴磁鐵(dependency magnet):其他許多類都得導入和使用它J.別重複自己
1.重複可能是軟體中一切邪惡的根源,許多原則與實踐規則都是為控制與消除重複而創建K.結構化編程
1.每個函數、函數中的每個代碼塊都應該有一個入口、一個出口。遵循這些規則,意味著在每個函數中只該有一個return語句,循環中不能有break或者continue語句,而且永永遠遠不能有任何的goto語句2.只有在大函數中這些規則才會有明顯好處,因為,只要函數保持短小,偶爾出現的return、break或continue語句沒有壞處,goto語句盡量避免L.如何寫出這樣的函數
1.打磨代碼,分解函數、修改名稱、消除重複2.縮短和重新安置方法、拆散類、保持測試通過四、注釋
1.若編程語言足夠有表達力,就不需要注釋2.注釋的恰當用法是彌補我們在用代碼表達意圖時遭遇的失敗。注釋總是一種失敗3.程序員應當負責將注釋保持在可維護、有關聯、精確的高度,更應該把力氣用在寫清楚代碼上,直接保證無須編寫注釋4.不準確的注釋要比沒注釋壞得多A.注釋不能美化糟糕的代碼
1.帶有少量注釋的整潔而有表達力的代碼,要比帶有大量注釋的零碎而複雜的代碼像樣得多2.與其花時間編寫解釋你搞出的糟糕的代碼的注釋,不如花時間清潔那堆糟糕的代碼B.用代碼來闡述
1.用代碼解釋你大部分的意圖,很多時候,簡單到只需要創建一個描述與注釋所言同一事物的函數即可C.好注釋
1.法律信息2.提供信息的注釋3.對意圖的解釋:提供某個決定後面的意圖4.闡釋:注釋把某些晦澀難懂的參數或返回值的意義翻譯為某種可讀形式5.警示6.TODO注釋:注意要清理7.放大:放大某種看來不合理之物的重要性8.公共API中的JavadocD.壞注釋
1.喃喃自語2.多餘的注釋3.誤導性注釋4.循規式注釋5.日誌式注釋6.廢話注釋7.可怕的廢話8.能用函數或變數時就別用注釋9.位置標記:如果標記欄不多,就會顯而易見,所以,盡量少用標記欄,只在特別有價值的時候用10.括弧後面的注釋11.歸屬與署名12.注釋掉的代碼13.HTML注釋14.非本地信息15.信息過多16.不明顯的聯繫17.函數頭18.非公共代碼中的Javadoc19.範例五、格式
A.格式的目的1.代碼格式關乎溝通,而溝通是專業開發者的頭等大事B.垂直格式
1.短文件通常比長文件易於理解2.源文件也要像報紙文章那樣 ,名稱應當簡單且一目了然,最頂部應該給出高層次概念和演算法,細節應該往下漸次展開3.幾乎所有的代碼都是從上往下讀,從左往右讀。每行展現一個表達式或一個子句,每代碼行展示一條完整的思路。這些思路用空白行區隔開來。4.如果說空白行隔開了概念,靠近的代碼行則暗示了它們之間的緊密關係5.除非有很好的理由,否則就不要把關係密切的概念放到不同的文件中,實際上,這也是避免使用protected變數的理由之一,應避免迫使讀者在源文件和類中跳來跳去6.變數聲明應儘可能靠近其使用位置,在函數頂部出現,循環的控制變數總是在循環語句中聲明7.實體變數在類的頂部聲明8.相關函數,若某個函數調用了另外一個,就應該把它們放到一起,而且調用者應該儘可能放在被調用者上面9.概念相關的代碼應該放到一起,相關性越強,彼此之間的距離就該越短10.我們想自上向下展示函數調用依賴順序,被調用的函數應該放在執行調用的函數下面,這就建立了一種自頂向下貫穿源代碼模塊的良好信息流C.橫向格式
1.儘力保持代碼行短小,遵循無需拖動滾動條到右邊的原則,最好不超過120個2.我們使用空格字元將彼此緊密相關的事物連接到一起,也用空格字元把相關性較弱的事物分隔開3.對齊,像是在強調不重要的東西,把目光從真正的意義上拉開4.如果有較長的列表需要做對齊處理,那問題就是在列表的長度上而不是對齊上5.程序員相當依賴縮進模式6.有時,while或for語句的語句體為空,如果無法避免,就確保空範圍體的縮進,用括弧包圍起來D.團隊規則
1.一組開發者應當認同一種模式風格,每個成員都應該採用那種風格2.好的軟體系統是由一系列讀起來不錯的代碼文件組成的,需要擁有一致和順暢的風格六、對象和數據結構
A.數據抽象1.隱藏實現關乎抽象,類並不簡單地用取值器和賦值器將其變數推向外部,而是曝露抽象介面,以便用戶無需了解數據的實現就能操作數據本體B.數據、對象的反對稱性
1.對象把數據隱藏於抽象之後,曝露操作數據的函數。數據結構曝露其數據,並沒有提供有意義的函數2.對象與數據結構之間的二分原理:* 過程式代碼(使用數據結構的代碼)便於在不改動既有數據結構的前提下添加新函數。面向對象代碼便於在不改動既有函數的前提下添加新類* 過程式代碼難以添加新數據結構,因為必須修改所有函數。面向對象代碼難以添加新函數,因為必須修改所有類C.得墨忒耳律
1.得墨忒耳律(The Law of Demeter):模塊不應了解它所操作對象的內部情形,意味著對象不應通過存取器曝露其內部結構,因為這樣更像是曝露而非隱藏其內部結構2.混合結構,一半是對象,一半是數據結構,應避免這種結構D.數據傳送對象
1.最為精練的數據結構,是一個只有公共變數、沒有函數的類,這種被稱為數據傳送對象,或DTO(Data Transfer Objects)。在與資料庫通信、或解析套接字傳遞的消息之類場景中2.JavaBean或Active Record3.不要塞進業務規則方法,把Active Record當做數據結構,並創建包含業務規則、隱藏內部數據(可能就是Active Record的實體)的獨立對象七、錯誤處理
1.錯誤處理很重要,但如果它搞亂了代碼邏輯,就是錯誤的做法A.使用異常而非返回碼
1.遇到錯誤時,最好拋出一個異常。調用代碼很整潔,其邏輯不會被錯誤處理搞亂B.先寫Try-Catch-Finally語句
1.異常的妙處之一是,它們在程序中定義了一個範圍。執行try-catch-finally語句中try部分的代碼時,你是在表明可隨時取消執行,並在catch語句中接續2.在某種意義上,try代碼塊就像是事務,catch代碼塊將程序維持在一種持續狀態3.在編寫可能拋出異常的代碼時,最好先寫try-catch-finally語句,能幫你定義代碼的用戶應該期待什麼,無論try代碼塊中執行的代碼出什麼錯都一樣C.使用不可控異常
1.可控異常的代價就是違反開放/閉合原則,得在catch語句和拋出異常處之間的每個方法簽名中聲明該異常2.可控異常意味著對軟體中較低層級的修改,都將波及較高層級的簽名D.給出異常發生的環境說明
1.拋出的每個異常,都應當提供足夠的環境說明,以便判斷錯誤的來源和處所2.應創建信息充分的錯誤消息,並和異常一起傳遞出去E.依調用者需要定義異常類
1.最重要的考慮是它們如何被捕獲2.將第三方API打包是個良好的實踐手段,降低了對每個第三方的依賴,也有助於模擬第三方調用F.定義常規流程
1.特例模式(SPECIAL CASE PATTERN,[Fowler]),創建一個類或配置一個對象,用來處理特例,異常行為被封裝到特例對象中G.別返回null值
1.返回null值,基本是在給自己增加工作量,也是在給調用者添亂,只要有一處沒檢查null值,應用程序就會失控H.別傳遞null值
1.將null值傳遞給其他方法更糟糕,除非API要求你向它傳遞null值,否則就要儘可能避免傳遞null值八、邊界
A.使用第三方代碼1.第三方程序包和框架提供者追求普適性,這樣就能在多個環境中工作,吸引廣泛的用戶2.我們建議不要將Map(或在邊界上的其他介面)在系統中傳遞,把它保留在類或近親類中,避免從API中返回邊界介面,或將介面作為參數傳遞給公共APIB.瀏覽和學習邊界
C.學習性測試的好處不只是免費1.學習性測試毫無成本,編寫測試是獲得這些知識(要使用的API)的容易而不會影響其他工作的途徑2.學習性測試確保第三方程序包按照我們想要的方式工作D.使用尚不存在的代碼
1.編寫我們想得到的介面,好處之一是它在我們控制之下,有助於保持客戶代碼更可讀,且集中於它該完成的工作E.整潔的邊界
1.邊界上的改動,有良好的軟體設計,無需巨大投入和重寫即可進行修改2.邊界上的代碼需要清晰的分割和定義了期望的測試。依靠你能控制的東西,好過依靠你控制不了的東西,免得日後受它控制3.可以使用ADAPTER模式將我們的介面轉換為第三方提供的介面九、單元測試
A.TDD三定律1.在編寫能通過的單元測試前,不可編寫生產代碼2.只可編寫剛好無法通過的單元測試,不能編譯也算不通過3.只可編寫剛好足以通過當前失敗測試的生產代碼B.保持測試整潔
1.臟測試等同於沒測試,測試必須隨生產代碼的演進而修改,測試越臟,就越難修改2.測試代碼和生產代碼一樣重要,它需要被思考、被設計和被照料,它該像生產代碼一般保持整潔3.如果測試不能保持整潔,你就會失去它們,沒有了測試,你就會失去保證生產代碼可擴展的一切要素C.整潔的測試
1.三個要素:可讀性、可讀性和可讀性,明確、簡潔還有足夠的表達力2.構造-操作-檢驗(BUILD-OPERATE-CHECK)模式,第一個環節構造測試數據,第二個環節操作測試數據,第三個部分檢驗操作是否得到期望的結果3.守規矩的開發者也將他們的測試代碼重構為更簡潔和具有表達力的形式D.每個測試一個斷言
1.JUnit中每個測試函數都應該有且只有一個斷言語句2.最好的說法是單個測試中的斷言數量應該最小化3.更好一些的規則或許是每個測試函數中只測試一個概念4.最佳規則是應該儘可能減少每個概念的斷言數量,每個測試函數只測試一個概念E.F.I.R.S.T
1.快速(Fast)測試應該夠快2.獨立(Independent)測試應該相互獨立3.可重複(Repeatable)測試應當可在任何環境中重複通過4.自足驗證(Self-Validating)測試應該有布爾值輸出5.及時(Timely)測試應及時編寫十、類
A.類的組織1.類應該從一級變數列表開始,如果有公共靜態變數,應該先出現,然後是私有靜態變數,以及實體變數,很少會有公共變數2.公共函數應該跟在變數列表之後3.保持變數和工具函數的私有性,但並不執著於此B.類應該短小
1.第一規則是類應該短小,第二規則是還要更短小2.衡量方法,計算權責(responsibility)3.類的名稱應當描述其權責,如果無法為某個類命以精確的名稱,這個類大概就太長了,類名越含混,該類越有可能擁有過多權責4.單一權責原則(SRP)認為,類或模塊應有且只有一條加以修改的理由5.系統應該由許多短小的類而不是少量巨大的類組成,每個小類封裝一個權責,只有一個修改的原因,並與少數其他類一起協同達成期望的系統行為6.方法操作的變數越多,就越黏聚到類上,如果一個類的每個變數都被每個方法所使用,則該類具有最大的內聚性7.保持函數和參數列表短小的策略,有時會導致為一組子集方法所用的實體變數數量增加。出現這種情況時,往往意味著至少有一個類要從大類中掙扎出來。你應當嘗試將這些變數和方法分拆到兩個或多個類中,讓新的類更為內聚8.將大函數拆為許多小函數,往往也是將類拆分為多個小類的時機C.為了修改而組織
1.在整潔的系統中,我們對類加以組織,以降低修改的風險2.開放-閉合原則(OCP):類應當對擴展開放,對修改封閉3.在理想系統中,我們通過擴展系統而非修改現有代碼來添加新特性4.依賴倒置原則(Dependency Inversion Principle,DIP),類應該依賴於抽象而不是依賴於具體細節十一、系統
A.如何建造一個城市1.每個城市都有一組人管理不同的部分,有人負責全局,其他人負責細節2.深化出恰當的抽象等級和模塊,好讓個人和他們所管理的「組件」即便在不了解全局時也能有效地運轉B.將系統的構造與使用分開
1.構造與使用是非常不一樣的過程2.軟體系統應將啟始過程和啟始過程之後的運行時邏輯分離開,在啟始過程中構建應用對象,也會存在互相纏結的依賴關係3.將構造與使用分開的方法之一是將全部構造過程搬遷到main或被稱為main的模塊中,設計系統的其餘部分時,假設所有對象都已正確構造和設置4.可以使用抽象工廠模式讓應用自行控制何時創建對象,但構造的細節卻隔離於應用程序代碼之外5.控制反轉將第二權責從對象中拿出來,轉移到另一個專註於此的對象中,從而遵循了單一權責原則。在依賴管理情景中,對象不應負責實體化對自身的依賴,反之,它應當將這份權責移交給其他「有權力」的機制,從而實現控制的反轉C.擴容
1.「一開始就做對系統」純屬神話,反之,我們應該只去實現今天的用戶故事,然後重構,明天再擴展系統、實現新的用戶故事,這就是迭代和增量敏捷的精髓所在。測試驅動開發、重構以及它們打造出的整潔代碼,在代碼層面保證了這個過程的實現2.軟體系統與物理系統可以類比。它們的架構都可以遞增式的增長,只要我們持續將關注面恰當地切分3.持久化之類關注面傾向於橫貫某個領域的天然對象邊界D.Java代理
1.適用於簡單情況,例如在單獨的對象或類中包裝方法調用2.舊式的Java對象(Plain-Old Java Object, POJO)E.純Java AOP框架
F.AspectJ的方面G.測試驅動系統架構1.通過方面式(AOP)的手段切分關注面的威力不可低估。假使你能用POJO編寫應用程序的領域邏輯,在代碼層面與架構關注面分離開,就有可能真正地用測試來驅動架構2.沒必要先做大設計(Big Design Up Front,BDUF),BDUF甚至是有害的,它阻礙改進,因為心理上會抵制丟棄即成之事,也因為架構上的方案選擇影響到後續的設計思路3.我們可以從「簡單自然」但切分良好的架構開始做軟體項目,快速交付可工作的用戶故事,隨著規模的增長添加更多基礎架構4.最佳的系統架構由模塊化的關注面領域組成,每個關注面均用純Java(或其他語言)對象實現,不同的領域之間用最不具有侵害性的方面或類方面工具整合起來,這種架構能測試驅動,就像代碼一樣H.優化決策
1.模塊化和關注面切分成就了分散化管理和決策2.延遲決策至最後一刻也是好手段,它讓我們能夠基於最有可能的信息做出選擇3.擁有模塊化關注面的POJO系統提供的敏捷能力,允許我們基於最新的知識做出優化的、時機剛好的決策,決策的複雜性也降低了I.明智使用添加了可論證價值的標準
1.有了標準,就更易復用想法和組件、僱用擁有相關經驗的人才、封裝好點子,以及將組件連接起來。不過,創立標準的過程有時卻漫長到行業等不及的程度,有些標準沒能與它要服務的採用者的真實需求相結合J.系統需要領域特定語言
1.領域特定語言(Domain-Specific Language, DSL)是一種單獨的小型腳本語言或以標準語言寫就的API,領域專家可以用它編寫讀像是組織嚴謹的散文一般的代碼2.領域特定語言允許所有抽象層級和應用程序中的所有領域,從高級策略到底層細節,使用POJO來表達十二、迭進
A.通過迭進設計達到整潔目的
1.「簡單規則」:
* 運行所有測試
* 不可重複* 表達了程序員的意圖* 儘可能減少類和方法的數量* 以上規則按其重要程序排列
B.簡單設計原則1:運行所有測試
1.設計必須製造出如預期一般工作的系統,這是首要因素2.全面測試並持續通過所有測試的系統,就是可測試的系統,不可驗證的系統,絕不應部署3.只要系統可測試,就會導向保持類短小且目的單一的設計方案4.緊耦合的代碼難以編寫測試5.遵循有關編寫測試並持續運行測試的簡單、明確的規則,系統就會更貼近OO低耦合度、高內聚度的目標,編寫測試引致更好的設計C.簡單設計原則2-4:重構
1.有了測試,就能保持代碼和類的整潔,方法就是遞增式地重構代碼2.測試消除了對清理代碼就會破壞代碼的恐懼D.不可重複
1.重複是擁有良好設計系統的大敵2.極其雷同的代碼行當然是重複,還有實現上的重複等其他一些形態3.「小規模復用」可大量降低系統複雜性,要想實現大規模復用,必須理解如何實現小規模復用4.模板方法模式是一種移除高層級重複的通用技巧E.表達力
1.軟體項目的主要成本在於長期維護,代碼應當清晰地表達其作者的意圖2.可以通過選用好名稱來表達3.可以通過保持函數和類尺寸短小來表達4.可以通過採用標準命名法來表達5.編寫良好的單元測試也具有表達性6.做到有表達力的最重要方式是嘗試F.儘可能少的類和方法
1.類和方法的數量太多,有時是由毫無意義的教條主義導致的,應該採用更實用的手段2.目標是在保持函數和類短小的同時,保持整個系統短小精悍十三、並發編程
A.為什麼要並發
1.並發是一種解耦策略,它幫助我們把做什麼(目的)和何時(時機)做分解開
2.解耦目的與時機能明顯地改進應用程序的吞吐量和結構
3.單線程程序許多時間花在等待web套接字I/O結束上面,通過採用同時訪問多個站點的多線程演算法,就能改進性能
4.常見的迷思和誤解
* 並發總能改進性能:只在多個線程或處理器之間能分享大量等待時間的時候管用
* 編寫並發程序無需修改設計:可能與單線程系統的設計極不相同* 在採用web或ejb容器時,理解並發問題並不重要
5.有關編寫並發軟體的中肯的說法:
* 並發會在性能和編寫額外代碼上增加一些開銷
* 正確的並發是複雜的,即使對於簡單的問題也是如此* 並發缺陷並非總能重現,所以常被看做偶發事件而忽略,未被當做真的缺陷看待* 並發常常需要對設計策略的根本性修改
B.挑戰
1.線程在執行代碼時有許多可能路徑可行,有些路徑會產生錯誤的結果C.並發防禦原則
1.單一權責原則(SRP):方法/類/組件應當只有一個修改的理由
* 並發相關代碼有自己的開發、修改和調優生命周期
* 開發相關代碼有自己要對付的挑戰,和非並發相關代碼不同* 即使沒有周邊應用程序增加的負擔,寫得不好的並發代碼可能的出錯方式數量也已經足具有挑戰性* 建議:分離並發相關代碼與其他代碼
2.推論:限制數據作用域
* 採用synchronized關鍵字在代碼中保護一塊使用共享對象的臨界區(critical section)
* 建議:謹記數據封閉;嚴格限制對可能被共享的數據的訪問
3.推論:使用數據複本
* 一開始就避免共享數據,複製對象並以只讀方式對待,或複製對象,從多個線程收集所有複本的結果,並在單個線程中合併這些結果
4.推論:線程應儘可能地獨立
* 讓每個線程在自己的世界中存在,不與其他線程共享數據
* 建議:嘗試將數據分解到可被獨立線程(可能在不同處理器上)操作的獨立子集
D.了解Java庫
1.要注意:
* 使用類庫提供的線程安全群集
* 使用executor框架(executor framework)執行無關任務* 儘可能使用非鎖定解決方案* 有幾個類並不是線程安全的
E.了解執行模型
1.一些基礎定義
* 限定資源:並發環境中有著固定尺寸或數量的資源
* 互斥:每一時刻僅有一個線程能訪問共享數據或共享資源* 線程飢餓:一個或一組線程在很長時間內或永久被禁止* 死鎖:兩個或多個線程互相等待執行結束。每個線程都擁有其他線程需要的資源,行不到其他線程擁有的資源,就無法終止* 活鎖:執行次序一致的線程,每個都想要起步,但發現其他線程已經「在路上」。由於競步的原因,線程會持續嘗試起步,但在很長時間內卻無法如願,甚至永遠無法啟動
2.生產者-消費者模型:一個或多個生產者線程創建某些工作,並置於緩存或隊列中。一個或多個消費者線程從隊列中獲取並完成這些工作。生產者消費者之間的隊列是一種限定資源
3.讀者-作者模型:協調讀者線程,不去讀作者線程正在更新的信息(反之亦然),這是一種辛苦的平衡工作,作者線程傾向於長期鎖定許多讀者純種,從而導致吞吐量問題4.宴席哲學家5.建議學習這些基礎演算法,理解其解決方案F.警惕同步方法之間的依賴
1.同步方法之間的依賴會導致並發代碼中的狡猾缺陷,建議避免使用一個共享對象的多個方法2.基於客戶端的鎖定:客戶端代碼在調用第一個方法前鎖定服務端,確保鎖的範圍覆蓋了調用最後一個方法的代碼3.基於服務端的鎖定:在服務端內創建鎖定服務端的方法,調用所有方法,然後解鎖。讓客戶端代碼調用新方法4.適配服務端:創建執行鎖定的中間層。這是一種基於服務端的鎖定的例子,但不修改原始服務端代碼G.保持同步區域微小
1.同一個鎖維護的所有代碼區域在任一時刻保證只有一個線程執行,因為它們帶來了延遲和額外開銷,臨界區應該被保護起來,應該儘可能少地設計臨界區H.很維編寫正確的關閉代碼
1.平靜關閉很難做到,常見問題與死鎖有關,線程一直等待永遠不會到來的信號2.建議:儘早考慮關閉問題,儘早令其工作正常I.測試線程代碼
1.建議:編寫有潛力曝露問題的測試,在不同的編程配置、系統配置和負載條件下頻繁運行。如果測試失敗,跟蹤錯誤。別因為後來測試通過了後來的運行就忽略失敗2.將偽失敗看作可能的線程問題:線程代碼導致「不可能失敗的」失敗,不要將系統錯誤歸咎於偶發事件3.先使非線程代碼可工作:不要同時追蹤非線程缺陷和線程缺陷,確保代碼在線程之外可工作4.編寫可插拔的線程代碼,能在不同的配置環境下運行5.編寫可調整的線程代碼:允許線程依據吞吐量和系統使用率自我調整6.運行多於處理器數量的線程:任務交換越頻繁,越有可能找到錯過臨界區域導致死鎖的代碼7.在不同平台上運行:儘早並經常地在所有目標平台上運行線程代碼8.裝置試錯代碼:增加對Object.wait()、Object.sleep()、Object.yield()、Object.priority()等方法的調用,改變代碼執行順序,硬編碼或自動化十四、逐步改進
1.要編寫清潔代碼,必須先寫骯髒代碼,然後再清理它2.毀壞程序的最好方法之一就是以改進之名大動其結構十五、JUnit內幕
十六、重構SerialDate十七、味道與啟發A.注釋1.不恰當的信息:注釋只應該描述有關代碼和設計的技術性信息2.廢棄的注釋3.冗餘的注釋4.糟糕的注釋:別閑扯,別畫蛇添足,保持簡潔5.注釋掉的代碼:刪除它B.環境
1.需要多步才能實現的構建:構建系統應該是單步的小操作2.需要多步才能做到的測試:應當能夠發出單個指令就可以運行全部單元測試C.函數
1.過多的參數:參數量應該少,三個以上的參數非常值得質疑2.輸出參數:輸出參數違反直覺,直接修改它所有對象的狀態3.標識參數:布爾值參數大聲宣告函數做了不止一件事,應該消滅掉4.死函數:永不被調用的方法應該丟棄D.一般性問題:
1.一個源文件中存在多種語言:儘力減少源文件中額外語言的數量和範圍2.明顯的行為未被實現:遵循「最小驚異原則」(The principle of Least Surprise),函數或類應該實現其他程序員有理由期待的行為 3.不正確的邊界行為:別依賴直覺,追索每種邊界條件,並編寫測試4.忽視安全5.重複:看到重複代碼,都代表遺漏了抽象6.在錯誤的抽象層級上的代碼:創建分離較高層級一般性概念(抽象類)與較低層級細節概念(派生類)的抽象模型7.基類依賴於派生類:基類對派生類應該一無所知8.信息過多:設計良好的模塊有著非常小的介面,限制類或模塊中暴露的介面數量,類中的方法越少越好,隱藏你的數據,隱藏你的工具函數,隱藏常量和臨時變數9.死代碼:刪除掉10.垂直分隔:變數和函數應該在靠近被使用的地方定義,垂直距離要短11.前後不一致:從一而終,可以追溯到最小驚異原則,讓代碼更加易於閱讀和修改12.混淆視聽:保持源文件整潔,良好地組織,不被搞亂13.人為耦合:不互相依賴的東西不該耦合14.特性依戀:類的方法只應對其所屬類中的變數和函數感興趣,不該垂青其他類中的變數和函數15.選擇運算元參數:使用多個函數,通常優於向單個函數傳遞某些代碼來選擇函數行為 16.晦澀的意圖:代碼要儘可能具有表達力17.位置錯誤的權責:代碼應該放在讀者自然而然期待它所有的地方18.不恰當的靜態方法:如果的確需要靜態函數,確保沒機會打算讓它有多態行為19.使用解釋性變數:讓程序可讀的最有力方法之一就是將計算過程打散成用有意義的單詞命名的變數中放置的中間值20.函數名稱應該表達其行為21.理解演算法:在你認為自己完成某個函數之前,確認自己理解了它是怎麼工作的,你必須知道解決方案是正確的22.把邏輯依賴改為物理依賴:依賴模塊不應對被依賴者模塊有假定,它應當明確地詢問後者全部信息23.用多態替代if/Else或Switch/Case,「單個switch」規則:對於給定的選擇類型,不應有多於一個switch語句24.遵循標準約定,遵循基於通用行業規範的一套編碼標準25.用命名常量替代魔術數,在代碼中出現原始形態數字通常來說是壞現象,有些常量與非常具有自我解釋能力的代碼協同工作時,就不必總是需要命名常量來隱藏了。「魔術數」泛指任何不能自我描述的符號26.準確,在代碼中做決定時,確認自己足夠準確,明確自己為何要這麼做,如果遇到異常情況如何處理27.結構甚於約定28.封裝條件,如果沒有if或while語句的上下文,布爾邏輯就難以理解,應該把解釋了條件意圖的函數抽離出來29.避免否定性條件,儘可能將條件表示為肯定形式30.函數只該做一件事31.掩蔽時序耦合,排列函數參數,好讓它們被調用的次序顯而易見32.別隨意,構建代碼需要理由,而且理由應與代碼結構相契合33.封裝邊界條件,把處理邊界條件的代碼集中到一處,不要散落於代碼中34.函數應該只在一個抽象層級上,函數中的語句應該在同一抽象層級上,該層級應該是函數名所示操作的下一層35.在較高層級放置可配置數據,如果你有個已知並該在較高抽象層級的默認常量或配置值,不要將它埋藏到較低層級的函數中36.避免傳遞瀏覽,讓直接協作者提供所需的全部服務,不必逛遍系統的對象全圖,搜尋我們要調用的方法E.Java
1.通過使用通配符避免過長的導入清單2.不要繼承常量,應該直接導入常量類3.常量 vs. 枚舉,放心使用枚舉F.名稱
1.採用描述性名稱,事物的意義隨著軟體的演化而變化,要經常性地重新估量名稱是否恰當2.名稱應與抽象層級相符,不要取溝通實現的名稱;取反映類或函數抽象層級的名稱3.儘可能使用標準命名法4.無歧義的名稱,選用不會混淆函數或變數意義的名稱5.為較大作用範圍選用較長名稱6.避免編碼,不要用匈牙利命名法污染你的名稱7.名稱應該說明副作用G.測試
1.測試不足,一套測試應該測到所有可能失敗的東西
2.使用覆蓋率工具,能彙報你測試策略中的缺口
3.別略過小測試
4.被忽略的測試就是對不確定事物的疑問
5.測試邊界條件
6.全面測試相近的缺陷
7.測試失敗的模式有啟發性,完整的測試用例,按合理的順序排列,能暴露出模式
8.測試覆蓋率的模式有啟發性
9.測試應該快速
推薦閱讀: