摘要:Kent Beck是XP(Extreme Programming)的創始人,有17年的面向對象的編程經驗,他倡導軟體開發的模式定義。本文作者分享了與Kent Beck一起編程所學到的編程價值觀和設計理念以及自己的親身感受,希望對各位開發人員有用!
我、Stig、Krzysztof、Jakub和Kent Beck在2012年的Iterate Code Camp花了一個星期時間一起做了個項目,進行最佳實踐性的編程學習。我們想和大家分享一下我們在這個過程中學習到的寶貴經驗及教訓,以使我們能成為更好的程序員(或者至少我們是這樣認為的)。
編程風格背後的價值觀
在整個實踐過程中,我們漸漸學會了遵守編程最基本的三個價值觀:溝通、簡單和靈活(按照重要性進行排序),這三個價值觀應該被所有開發人員所銘記。在下面我們將會對它們進行簡單介紹,另外你也可以在Kent的《實現模式》(Implementation Patterns)一書中找到更詳細的說明以及一些基本的實踐。
溝通
程序的可讀性往往高於程序如何去寫,任何一個程序員都能寫出機器讀得懂的代碼,而一個優秀的程序員應該寫出別人可以理解的代碼。代碼作為傳達設計理念的工具,在編寫的時候應該清楚地表達出設計思想。(就拿典型的企業系統來說,在5——15年內代碼都會被大量修改,而每次去執行修改的人肯定不會是同一個人,因此代碼要更加通俗易懂,這樣修改起來才輕鬆。
簡單
消除不必要的複雜性,各級應用都應該以簡單為原則。簡單可以使程序更容易理解和更利於溝通;通過溝通更容易實現簡單。
靈活
現在做出一個可行的決定,並保留可以將來修改它的靈活度,這是好的軟體開發的關鍵。——Kent Beck的《Implementation Patterns》
程序應該靈活地修改,使一些常見的修改變得簡單,至少要比現在簡單。複雜性通常來自於過度的靈活性,但是如果沒有靈活性,那麼它就如同垃圾。最好的一種靈活性就是來自簡單廣泛的測試。試圖通過設計phases之類行為來增加靈活度,最終得到的通常是“複雜的靈活”。大多數時候,對於程序哪些地方需要被修改,在沒有足夠信息的前提下很難做出正確的決定。所以適當地推遲決定,直到最後判斷正確后再做出一個有效和有用的決策。
概括
編寫簡單、通俗易懂的代碼,會讓你的工作夥伴或者未來的系統維護人員花很少的精力就可以輕易代碼。(當然,這也需要一定的技能等級和專業知識。)你無法預見需求變化,所以保持代碼簡單、靈活,使其演變可以跟上需求的變化和時間的推移。
學習要點1 你並不需要它
今天將演示什麼?測試開始時做什麼?
在開始之前,我們設立了一個非常有趣和富有挑戰的主題。我們決定盡最大的努力嘗試一個高伸縮性的、分散式資料庫。我們花了幾個小時討論如何實現,畢竟這裡有許多事情需要考慮:同步策略、哈希一致、集群成員自動發現、衝突解決方案等等。如果只有5天時間,你需要計劃如何用最簡單的方式實現。需要做什麼?如何跳過?對嗎?錯,Kent只是問我們在最後一天將演示什麼和如何測試。
這最終會被證明是個非常聰明的做法,因為我們實際上可實現的功能只有一個小小的子集,對迄今遇到的問題進行總結,然後再進一步做出更加理智的決策。我們發現,任何超過10分鐘以上的討論90%的時間都被浪費啦!但這並不意味那個計劃就是失敗的(雖然產生的計劃一般都是無用的),它只是意味著我們要做的會比實際需要多。
因此,我們寧願選擇不斷接受反饋和經驗來完善前期計劃。問問自己:在下次我將要展示什麼?寫什麼樣的反饋能夠反映自己,指導我今後的開發工作。
學習要點2 編寫高級測試來指導開發
我們第二天的目標是通過編寫一個實例進行同步和演示,忘掉第一個實例然後直接從第二個實例中解讀(複製)數據。我們僅僅跟隨著這些步驟編寫相應的測試。
- List<Graft> grafts = Graft.getTwoGrafts();
- Graft first = grafts.get(0);
- Graft second = grafts.get(1);
- first.createNode().put("key", "value")
- first.kill();
- assertNotNull(second.getNodeByProperty("key", "value"));
(當然,這個API之後會變得普通)
現在有意思的是,這並不是個單元測試,它是個基本的集成測試,少用點技術術語就是一個故事驅動的高層次功能測試,是客戶比較喜歡的功能。如果是一個單元測試,那麼它會告訴你,這個類和預期打算的一樣,而如果是一個故事測試,那麼結果會是:“此功能和預期的一樣”。
我一直認為TDD只適合單元/類級別的測試,這樣的觀點被證明是錯誤的,TDD還適合更高級別的測試。它包含了一些非常有意思的屬性:
現在,根據測試金字塔來看,故事測試用例明顯要比單元測試要少,而且故事測試用例並不會測試所有可能性。難道這意味著需要在測完所有的故事測試用例后,再在小規模的單元測試中重複一遍?答案是否定的,那是完全沒有意義的。回到靈活性原則和改變方式上,只有當你需要的時候才會構建額外的單元測試,例如在這幾種情況下:第一個故事測試沒有完全捕捉正確、發現一個非常重要的特殊案例、你想專註於整個解決方案裡面的一部分。猜測故障點跟猜測設計一樣,完全是在浪費時間。
學習要點3 單元測試的最佳實踐
通常,會為了驗證某個想法而開始一段測試,但我們並沒有十足的把握能確保成功。因此,提供一個最佳實踐來指導或幫助你實現最終結果。首先,提出一個要求,然後在集中精力去考慮如何實現。這就是我們在Graft項目中進行同步策略測試的最佳實踐方法。
在測試裡面寫實現方法
確定功能后再開始編寫測試用例,而不是提前思考如何組織(創建哪些類?在哪裡進行整合?是否使用一個工廠類或工廠方法),為什麼不直接在測試方法裡面寫代碼?上面提到的那些因素可以以後分析。這樣你就可以一心一意地撰寫功能測試報告。此外,通過推遲內部組織實施,你將會有更多的時間去思考和決定,最後你會得到一個非常好的解決方案。
關鍵原則:重點、避免過早決策
自底向下的設計
避免:
從小到大,先從一小部分的功能開始實現,然後再一步步整合形成一個複雜的模塊。不要因為各個模塊間的依賴關係而感到心煩意亂,在真正實現整合和替代之前先給它們進行簡單地備份。使用這種技術就無需在最初的時候與設計方案相綁定。在這個過程中,不僅需要經驗還需根據點直覺,連同TDD會有更好的設計和實現方案展現出來。
在沒有形成最終解決方案的時候,我們發現這個方法非常有用。在開發Graft時,我們並沒有預先設計好整個應用程序。在第一天我們只是挑選了一個用戶案例去實現,然後在接下的日子裡,我們也是在挑選案例然後進行實現。
行動與要求
我們的Graft資料庫有一個接受用戶命令的telnet-like介面。參照下面測試addComment的兩個簡單地變化:
- // Test 1
- Graft db = ...; this.subject = new CommandProcessor(db);
- subject.process("addComment eventId my_comment");
- assertThat(subject.process("getComments eventId")).isEqualTo("my_comment");
- // Test 2 (same setUp)
- subject.process("addComment eventId my_comment");
- assertThat(db.getComments("eventId")).containsOnly("my_comment");
在Test1裡面,當對addComment命令進行測試時,直接使用getComments這個命令來檢查結果狀態。在整個測試過程中,只使用了一個單獨的API入口點——subject。Test2是直接訪問底層資料庫實例並且使用其API來獲取數據。與此同時,subject這個API也訪問底層資料庫。
因此Test1並不是真正意義上的“單元”測試,因為它還依賴另一個測試類。而Test2則更加專註並且寫法更加簡單,它直接訪問目標數據結構,對源代碼進行正確檢查。
我們將繼續討論像Test1那樣的測試,在同等級上,它會完成所有相同的操作,即基於同一級別的公共API對象測試會更好。這裡的“更好”是指容易理解、更加重要和穩定可維護,因為它們不是耦合到內部實現的功能測試。
像Test2的這種測試則更常見,無論是直接訪問底層(對象、屬性、資料庫……)還是通過模擬來直接對產生的副作用進行驗證。這些技術往往會導致耦合且對測試難於維護,這種技術應該限制在“私有單元測試”中,切勿混合公共和私有單元測試”。
還有一件值得我們的注意的事情,Kent任何時候都會關注自己在做什麼。專註意味著你會一心一意的做一件事情,不會受其他事情所影響,無論那件事情有多麼重要或者只是簡單修復。(備註:永遠不要說永遠)如果你在修復一個Bug的過程中還發現其他的事情,比如給一個類起更好的名字、刪除已死的代碼、修復一個未提交的Bug等。這些你都可以用列表記下來,等到手頭的Bug修復成功后再去落實。
并行(Parallel)設計
并行設計意味著當改變一個設計的時候,你要儘可能的保持在原來的需求上漸漸添加新元素,直至轉向新設計。在這個過程要不斷地確保正確性,這需要花費更大的精力,當然這樣做也會讓其更加安全和容易實現可恢復重構。
一個典型的并行設計案例是使用NoSQL資料庫替換RDBMS。開始時,你會把實現好的代碼寫入新的資料庫中,然後再把它同時寫進新舊這兩個系統裡面,並且從新的裡面讀取數據(也許這樣就可以與舊的相比較進行驗證),同時還在使用舊的資料庫數據。接下來你將開始實際使用NoSQL資料庫的數據,同時從舊DB讀/寫數據(這樣你可以方便地切換回來)。只有當新的DB被證明正確無誤時,才可以逐步刪除舊的DB。
一個小型的并行設計案例是用對象替換方法參數,例如notifuComment方法:
- - public void notifyComment(String message, String eventName, String user) {
- - notifications.add(user + ": commented on " + eventName + " " + message);
- ---
- + public void notifyComment(Edge target) {
- + notifications.add(target.getTo().getId() + ": commented on " + target.getFrom().getId() + " " + target.get("comment"));
步驟:
這樣做的好處是,代碼的正確性會得到保證並且可以隨時工作,你可以在任何時候提交或停止。
可恢復的重構
對大規模的代碼進行重構時,需要使用一些最佳實踐來保證代碼隨時可被恢復。上面介紹的并行設計是被實踐證明的,一種非常安全的重構步驟,在執行過程中不會破壞任何東西,代碼的正確性也會得到保證。
在重構過程中,不同的人會有不同的測試策略。但是我想說的,程序員在開發代碼時,應該有這樣的理念:我因編寫代碼而得到報酬,而不是為了測試,所以我希望編寫的代碼能夠達到一定水平以至於更少的被測試。(我認為,這種信心等級與工業標準相比還是比較高的,但這僅僅有點狂妄自大)如果我平時很少犯這種錯誤(比如在構造函數裡面設置了錯誤的變數),就不用對它進行測試。我比較傾向做一些有意義測試錯誤,所以我會額外關心那種複雜條件下的邏輯結構。當在一個團隊中編碼時,我會改變測試策略,非常小心的對代碼進行測試,最後對錯誤進行總結。
最後,我通過引用Kent的話:“how much testing to do”來結束這一主題。
對稱守則
對稱是一個非常抽象的概念,與溝通、簡單和靈活相比更加具體,但仍然是普通的。在Kent的《Implementation Patterns》一書中指出,對稱應該作為編程原則。
與不對稱相比而言,代碼的對稱性還是比較容易把握的。它更容易去讀和理解,所以對稱代碼會更加具體,再次引用Kent的話:
代碼的對稱性表現在代碼中,所有表達同一想法的地方都用相同的方法來表達。
想象一下代碼所要表達的想法,比如“從資料庫中獲取最後的更新文件”代碼需要執行幾次。如果方法名稱和執行順序都不同且彼此有很大的差別,那麼這樣的代碼就是不對稱的。問問自己:“這個方法是用來幹嘛的”。一個對稱代碼的例子是保持代碼在抽象層次上的一致性,比如方法。細心的讀者可能已經注意到,一致性是對稱的抽象層次,例如一致性的方法命名。但是,對稱是抽象的,它涉及更多的想法,而不是規則(如“駝峰式”里的類名和方法命名規則)。
你可以在GitHub下載我們此次在俱樂部所開發的源碼。
總結
我非常希望你們(親愛的讀者)能在平時的工作實踐中運用這些價值觀和設計原則,希望它們對你有用!
英文原文:Java Code Geeks
[火星人 ] 跟極限編程創始人Kent Beck學編程已經有531次圍觀