好的程式碼可以自己說話!

承香墨影發表於2019-03-01
162
162

你見過類似這樣的程式碼嗎?

code1
code1

我敢打賭,你肯定有過(或者在你的職業生涯中,某個時刻看到過)。這樣的程式碼,通常存在於一些遺留的系統中,並且通常是很舊的。當你需要閱讀這樣的程式碼的時候,你可能會感覺不太好。

這段程式碼的問題在於,它不僅太冗長,而更重要的是,它隱藏了業務邏輯(這短程式碼還有其他問題,我們將在後面講到)。在企業應用程式中,我們編寫程式碼來解決實際的業務問題。因此。我們不應該在修改程式碼的時候產生新的問題。請注意,當我們編寫”系統程式碼” 或者以高效能為目標的 Library 時,或者我們解決的問題在技術上太過複雜時,可以適度的犧牲可讀性。但即使如此,我們也應該小心翼翼的避免編寫隱藏邏輯的程式碼邏輯。

Robert C.Martin 在它的書《Clean Code : A Handbook of Agile Software Craftsmanship》 中提到過,”閱讀(程式碼)和寫作的時間比例,遠遠超過10 :1″。在一些遺留系統中,我發現自己花費大部分時間試圖理解如何閱讀程式碼,而不是能直接去閱讀程式碼本身的邏輯。測試和除錯這樣的系統也是非常棘手的。在大多數情況下,有一些特殊的、不尋常的方式去處理邏輯,而這將完全不同於你之前所理解的一切。

我們寫的內容應該像在訴說一個故事

程式碼不是例外。程式碼不應該隱藏,用於解決問題的業務邏輯或演算法。相反,它應該明確指出這些關鍵的業務邏輯或演算法。程式碼中使用的方法的名稱,方法的長度,甚至程式碼的格式應該看起來像問題已被處理的謹慎而專業。

那接下來看看,你對這短程式碼有什麼感覺?

calc
calc

這段程式碼,看著像是戰後的戰場,傷痕累累。我想每個會閱讀並修改這段程式碼的開發者,都討厭這樣的程式碼,並檢視從這個地獄中逃脫出去,而這將使得情況變的更糟糕。不同的編碼風格和糟糕的命名方式,清晰地表明,這段程式碼不止讓一個開發人員在這個地獄中輪迴。這聽起來像 破窗理論,不是嗎?理解這段程式碼的功能並不容易(不僅因為你看程式碼時會眼暈)。這段程式碼返回陣列的總和減去元素的數量。讓我們以更方便的方式來做到這一點。

sumMinusCount
sumMinusCount

現在,我們可以使用 Java 8 的流式程式設計方式,使我們的程式碼變的更加簡潔和可讀。

Clean Code(簡潔的程式碼)

Clean Code 不是為了讓我們的程式碼看起來漂亮,而是為了讓我們的程式碼更易於維護。當程式碼模糊不清時候,我們的大部分時間都將花在閱讀上。

因此,開發者的生產力降低了。模糊的程式碼的後果是,維護過它的開發人員通常會讓它變得更糟,就像我們前面看到的那樣。這樣做的原因並不是因為他們無法清理並重構這段程式碼,而是由於時間的限制,通常是時間不夠。

當我們編寫模糊的程式碼時,由於系統的體系結構/設計隱藏在程式碼中,所以很難估計修復 Bug 或實現新功能需要多長時間。因此,為了完成工作,我們最終以打補丁的方式,修復了問題或增加了功能,而這將增加新的技術債務。

另一方面,簡潔的程式碼顯示了作者的想法,所以即使在程式碼中存在一個錯誤,也很容易找到並修復它。簡潔的程式碼可以幫助我們長遠地加快程式設計速度。

對這些模糊的程式碼,如果想要解決它,可能需要花費幾個月(或更多)的時間來重構並清理它。但是公司通常不會接受發展將被暫停的代價,來讓開發者重構程式碼,這樣的機會非常的渺茫,除非已經到了業務無法繼續推動下去。

所以,我們還能做些什麼?

童子軍規則

正如 Robert C.Martin 說說,童子軍規則(The Boy Scout Rule)背後的思想相當的簡單:讓程式碼比你看到它的時候更乾淨! 每當你接觸舊程式碼的時候,你應該正確的清理和適當的重構它。不應該以打補丁的方式只改動你必須要改動的地方,這將使程式碼更難理解。

這個規則更多的是在說開發者應該擁有的心態,通過使系統更易於維護,從而讓他們的工作更加輕鬆容易。

我必須誠實的承認,在大多數情況下,處理遺留系統並不容易,特別是當沒有測試資源或者自動化測試程式碼不再維護的時候。但是在這篇文章中,我想關注一些我認為有用的一般性建議,來描述如何編寫更多具有表達性的程式碼。

在你開始寫之前,好好想想

開發人員應該在編寫程式碼之前清晰的認識到你正在做什麼?我們是使用程式碼在解決問題,程式碼只是媒介,而不是實際的解決方案。

因此,當我們編寫程式碼時,我們必須格外小心,以便可以讓我們編寫的程式碼,清晰的表明我們需要解決問題的解決方案。程式碼應該解決問題,而不是帶來新的問題。

你有沒有被要求做 程式碼審查(Code Review),當我們意識到程式碼中存在錯誤,唯一的解決辦法是從頭再次寫一遍?我看到許多開發人員一旦得到開發任務,就開始在 IDE 中輸入內容。他們認為,如果他們這樣做,他們看起來就像在工作。大多數情況下,這被證明是錯誤的方法,因為沒有經過思考,就開始編寫程式碼可能會導致錯誤向錯誤的方向發展。當然,一些非常有經驗的開發人員可以馬上開始編寫程式碼並朝正確的方向發展,但是大多數開發人員在實際編碼之前需要仔細想想。

customer
customer

這個例子中的的程式碼,並沒有什麼不好的。對吧?但是實際上,這裡使用了 策略模式 ,表明這端程式碼需要有一定的靈活性。而在這裡例子中,我們只實現了一個策略,沒有更多的實現,而這裡使用策略模式,可能會誤導讀者。一個策略模式是需要編寫更多的程式碼的,所以讀者自然可能會想到,讓前一個開發者使用策略模式的原因是什麼呢?YAGNI 原則表示,”你不會需要它”,但是這裡卻做了更多不必要的事情。預測未來我們將需要什麼,是很難預測的。在預測未來的需求上,有時候經驗是會有幫助的,但是大多數情況下,保持簡單是比較安全的。

設計模式幫助我們以一種通用的優雅方式來解決特定的問題。但是如果這樣的問題並不存在(例如前面的例子中,並不需要策略模式),之後程式碼的閱讀者將會被誤導,並認為這樣做是有必要的,是為了解決實際問題。需要特別說明一下,我並沒有反對任何設計模式,我也非常喜歡用它們,問題是有時候人們會去套用設計模式來解決問題,只是因為他們知道這個設計模式。

我們的工具集中有很多工具,我們應該有能力分清楚,何時是使用它們最恰當的時候。僅僅是因為框架或者庫被大多數開發者使用,是沒有意義的。我們必須知道它們能解決什麼問題,會存在什麼問題,並以一種不隱藏業務邏輯的方式來使用它們。

爭取表現力!

如今,許多程式語言都是支援流的。例如 Java、Kotlin、JavaScript 等來幫助我們編寫表示式程式碼。流已經用 “if” 語句取代了大段的迴圈。資料流幫助我們以一種宣告式的方式,更具有說明性的操作來進行資料轉換。如果你想找到一個集合中所有小於某個值的元素,迴圈迭代集合將沒有意義,只需要通過過濾器運算元據流就可以了。

Map、Filter 和 Reduce ,幾乎每一個支援流的語言都支援它。所以,每個人都可以理解你寫的東西,就像每個人都能理解一個迴圈或者一個 if 語句一樣。

有這樣的清晰處理邏輯的方式是強大的。首先,你不必測試這個功能,因為它們一定是穩定的。而你有沒有注意到第一個例子中的問題?當使用函數語言程式設計的方式,它將變的更加簡單。函數語言程式設計在這篇文章中,有很多好處,但是我重點介紹它來如何幫助程式碼提高可讀性。

第一個例子中,基於流的解決方案如下:

getProductNames2
getProductNames2

簡單而乾淨。很容易就理解它在做什麼。現在,再來看看下面的例子:

getPositiveNumbers
getPositiveNumbers

你是否期望當你呼叫這個方法額時候,方法的第二個引數將會被改變?這個方法是否按照所想的去做?方法名稱是否合適?你真的得到預期的結果了嗎?

那麼,現在呢?

getPositnumver
getPositnumver

在這個例子中,返回值是一個新的列表,沒有引數會受到影響。我們只是讀取引數併產生一個新的結果。理解這個方法現在做什麼以及如何使用它將變的更容易。這種方法可以很容易的與其它方法組合。

一般而言,組合是流和函數語言程式設計的最重要的好處之一。組合能使我們能夠在更高階別上進行資料的轉換、過濾等操作,並編寫更具說明性和表達性的程式碼,而不是舊的命令式風格。我們寫的程式碼表達了我們想要做的而不是如何完成!這是程式碼可讀性的重大改進。

把一個大問題分解成多個子問題,解決每一個子問題,然後組合這些解決方案,這將為解決初始問題提供瞭解決方案。

請注意,Java 8 中的 toList() 會返回一個可變的列表,而在函數語言程式設計中,我們通常使用不可變資料結構。不過,我們生成一個新的資料集合和將引數是為制度的,會有利於我們改進程式碼的可讀性。

編寫表示式程式碼不是一件容易的事情。有句 Albert Einstein 說過的名言,”如果你不能簡單的解釋它,你並不是真正的理解它”。所以,當我看到抽象層混合的邏輯程式碼時,例如與 DAO 互動的 UI 類,能直接與資料庫互動,又或者低層次的細節在不應該被暴露的地方暴露了。我們都知道單一職責原則的 SOLID 原則,但是關於這個問題一直受人詬病,因為有些時候很難做職責的劃分,這部分是它有爭議的關鍵。在程式碼中使用註釋來解釋程式碼,並不是一個解決方案,我們將在後面的文章中看到。我相信有人寫的越簡單、越具表達性的程式碼,他或者她對這個問題的理解就越清晰。

擁抱不變性

當物件的狀態發生變化,而我們沒有注意到它的時候,這真的會讓人困惑。使用返回值可以構造一半的物件也是很危險的,特別是當我們處理具有多個執行緒的程式時。共享這些物件真的很難做到正確。另一方面,不可變物件是執行緒安全的,也是快取的最佳選擇,因為它們的狀態不會改變。

但是為什麼人們選擇可變物件呢?我相信最有可能的原因是他們認為他們會獲得更好的結果,因為所使用的記憶體會更少,因為這些更改已經完成了。而且,讓一個物件的狀態在其整個生命週期中發生變化是很自然的。這是我們在 OOP 中學到的。這些年來,我們一直在寫程式,其中大部分的物件都是可變的。

如今,一個系統的記憶體數量比幾十年前大了幾個數量級。我們面臨的真正問題是可擴充套件性。處理器的速度不再像過去幾年那樣告訴的提高了,但是現在我們有了幾十個核心的盒子。所以,對於我們的規模來說,我們需要利用現在的情況。由於我們的程式需要能夠在多個核心上執行,所以我們需要以一種安全的方式編寫它們。通過使用可變物件,我們必須處理鎖定以確保其狀態的一致性。併發並不是一個小問題要解決。另一方面,由於它們的性質,不可變物件在多執行緒和處理器之間共享是固有安全的。而且,不需要同步的事實為建立具有低延遲和高吞吐量的系統提供了機會。因此,不變性是實現可擴充套件性的更安全的選擇。

除了可擴充套件性的好處,不變性使我們的程式碼更清潔。在上一節的第一個示例中,作為引數傳遞的集合在方法呼叫之後發生了更改。如果收藏是不可改變的,那麼這是被禁止的。因此,不變性會促使我們走向更好的解決方案。另外,由於狀態不可變,閱讀者不必記住他心中的狀態變化。閱讀者只需要將一個名稱與一個值關聯起來,而不記得變數的最新值。

程式必須是為人們閱讀而寫的,它只是恰巧讓機器執行了。

— Harold Abelson

這篇文章更多的是關於編寫更具可讀性和表達性的程式碼的一般建議。在將來的文章中,我們將討論生產程式碼和測試程式碼中的氣味。我們也將看到我們如何才能通過檢視我們的測試來在我們的生產程式碼中找到可能的設計問題。

敬請關注!

原文地址:

hackernoon.com/let-the-cod…

今天在承香墨影公眾號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、設計模式、虛擬機器、Kotlin、Linux、Web專案原始碼。

推薦閱讀:

相關文章