你見過類似這樣的代碼嗎? 我敢打賭,你肯定有過(或者在你的職業(yè)生涯中,某個時刻看到過)。這樣的代碼,通常存在于一些遺留的系統(tǒng)中,并且通常是很舊的。當(dāng)你需要閱讀這樣的代碼的時候,你可能會感覺不太好。 這段代碼的問題在于,它不僅太冗長,而更重要的是,它隱藏了業(yè)務(wù)邏輯(這短代碼還有其他問題,我們將在后面講到)。在企業(yè)應(yīng)用程序中,我們編寫代碼來解決實際的業(yè)務(wù)問題。因此。我們不應(yīng)該在修改代碼的時候產(chǎn)生新的問題。請注意,當(dāng)我們編寫'系統(tǒng)代碼' 或者以高性能為目標(biāo)的 Library 時,或者我們解決的問題在技術(shù)上太過復(fù)雜時,可以適度的犧牲可讀性。但即使如此,我們也應(yīng)該小心翼翼的避免編寫隱藏邏輯的代碼邏輯。 Robert C.Martin 在它的書《Clean Code : A Handbook of Agile Software Craftsmanship》 中提到過,'閱讀(代碼)和寫作的時間比例,遠(yuǎn)遠(yuǎn)超過10 :1'。在一些遺留系統(tǒng)中,我發(fā)現(xiàn)自己花費大部分時間試圖理解如何閱讀代碼,而不是能直接去閱讀代碼本身的邏輯。測試和調(diào)試這樣的系統(tǒng)也是非常棘手的。在大多數(shù)情況下,有一些特殊的、不尋常的方式去處理邏輯,而這將完全不同于你之前所理解的一切。 我們寫的內(nèi)容應(yīng)該像在訴說一個故事 代碼不是例外。代碼不應(yīng)該隱藏,用于解決問題的業(yè)務(wù)邏輯或算法。相反,它應(yīng)該明確指出這些關(guān)鍵的業(yè)務(wù)邏輯或算法。代碼中使用的方法的名稱,方法的長度,甚至代碼的格式應(yīng)該看起來像問題已被處理的謹(jǐn)慎而專業(yè)。 那接下來看看,你對這短代碼有什么感覺? 這段代碼,看著像是戰(zhàn)后的戰(zhàn)場,傷痕累累。我想每個會閱讀并修改這段代碼的開發(fā)者,都討厭這樣的代碼,并視圖從這個地獄中逃脫出去,而這將使得情況變的更糟糕。不同的編碼風(fēng)格和糟糕的命名方式,清晰地表明,這段代碼不止讓一個開發(fā)人員在這個地獄中輪回。這聽起來像 破窗理論,不是嗎?理解這段代碼的功能并不容易(不僅因為你看代碼時會眼暈)。這段代碼返回數(shù)組的總和減去元素的數(shù)量。讓我們以更方便的方式來做到這一點。 現(xiàn)在,我們可以使用 Java 8 的流式編程方式,使我們的代碼變的更加簡潔和可讀。 Clean Code(簡潔的代碼) Clean Code 不是為了讓我們的代碼看起來漂亮,而是為了讓我們的代碼更易于維護(hù)。當(dāng)代碼模糊不清時候,我們的大部分時間都將花在閱讀上。 因此,開發(fā)者的生產(chǎn)力降低了。模糊的代碼的后果是,維護(hù)過它的開發(fā)人員通常會讓它變得更糟,就像我們前面看到的那樣。這樣做的原因并不是因為他們無法清理并重構(gòu)這段代碼,而是由于時間的限制,通常是時間不夠。 當(dāng)我們編寫模糊的代碼時,由于系統(tǒng)的體系結(jié)構(gòu)/設(shè)計隱藏在代碼中,所以很難估計修復(fù) Bug 或?qū)崿F(xiàn)新功能需要多長時間。因此,為了完成工作,我們最終以打補丁的方式,修復(fù)了問題或增加了功能,而這將增加新的技術(shù)債務(wù)。 另一方面,簡潔的代碼顯示了作者的想法,所以即使在代碼中存在一個錯誤,也很容易找到并修復(fù)它。簡潔的代碼可以幫助我們長遠(yuǎn)地加快編程速度。 對這些模糊的代碼,如果想要解決它,可能需要花費幾個月(或更多)的時間來重構(gòu)并清理它。但是公司通常不會接受發(fā)展將被暫停的代價,來讓開發(fā)者重構(gòu)代碼,這樣的機會非常的渺茫,除非已經(jīng)到了業(yè)務(wù)無法繼續(xù)推動下去。 所以,我們還能做些什么? 童子軍規(guī)則 正如 Robert C.Martin 說說,童子軍規(guī)則(The Boy Scout Rule)背后的思想相當(dāng)?shù)暮唵危?strong>讓代碼比你看到它的時候更干凈! 每當(dāng)你接觸舊代碼的時候,你應(yīng)該正確的清理和適當(dāng)?shù)闹貥?gòu)它。不應(yīng)該以打補丁的方式只改動你必須要改動的地方,這將使代碼更難理解。 這個規(guī)則更多的是在說開發(fā)者應(yīng)該擁有的心態(tài),通過使系統(tǒng)更易于維護(hù),從而讓他們的工作更加輕松容易。 我必須誠實的承認(rèn),在大多數(shù)情況下,處理遺留系統(tǒng)并不容易,特別是當(dāng)沒有測試資源或者自動化測試代碼不再維護(hù)的時候。但是在這篇文章中,我想關(guān)注一些我認(rèn)為有用的一般性建議,來描述如何編寫更多具有表達(dá)性的代碼。 在你開始寫之前,好好想想 開發(fā)人員應(yīng)該在編寫代碼之前清晰的認(rèn)識到你正在做什么?我們是使用代碼在解決問題,代碼只是媒介,而不是實際的解決方案。 因此,當(dāng)我們編寫代碼時,我們必須格外小心,以便可以讓我們編寫的代碼,清晰的表明我們需要解決問題的解決方案。代碼應(yīng)該解決問題,而不是帶來新的問題。 你有沒有被要求做 代碼審查(Code Review),當(dāng)我們意識到代碼中存在錯誤,唯一的解決辦法是從頭再次寫一遍?我看到許多開發(fā)人員一旦得到開發(fā)任務(wù),就開始在 IDE 中輸入內(nèi)容。他們認(rèn)為,如果他們這樣做,他們看起來就像在工作。大多數(shù)情況下,這被證明是錯誤的方法,因為沒有經(jīng)過思考,就開始編寫代碼可能會導(dǎo)致錯誤向錯誤的方向發(fā)展。當(dāng)然,一些非常有經(jīng)驗的開發(fā)人員可以馬上開始編寫代碼并朝正確的方向發(fā)展,但是大多數(shù)開發(fā)人員在實際編碼之前需要仔細(xì)想想。 這個例子中的的代碼,并沒有什么不好的。對吧?但是實際上,這里使用了 策略模式 ,表明這端代碼需要有一定的靈活性。而在這里例子中,我們只實現(xiàn)了一個策略,沒有更多的實現(xiàn),而這里使用策略模式,可能會誤導(dǎo)讀者。一個策略模式是需要編寫更多的代碼的,所以讀者自然可能會想到,讓前一個開發(fā)者使用策略模式的原因是什么呢?YAGNI 原則表示,'你不會需要它',但是這里卻做了更多不必要的事情。預(yù)測未來我們將需要什么,是很難預(yù)測的。在預(yù)測未來的需求上,有時候經(jīng)驗是會有幫助的,但是大多數(shù)情況下,保持簡單是比較安全的。 設(shè)計模式幫助我們以一種通用的優(yōu)雅方式來解決特定的問題。但是如果這樣的問題并不存在(例如前面的例子中,并不需要策略模式),之后代碼的閱讀者將會被誤導(dǎo),并認(rèn)為這樣做是有必要的,是為了解決實際問題。需要特別說明一下,我并沒有反對任何設(shè)計模式,我也非常喜歡用它們,問題是有時候人們會去套用設(shè)計模式來解決問題,只是因為他們知道這個設(shè)計模式。 我們的工具集中有很多工具,我們應(yīng)該有能力分清楚,何時是使用它們最恰當(dāng)?shù)臅r候。僅僅是因為框架或者庫被大多數(shù)開發(fā)者使用,是沒有意義的。我們必須知道它們能解決什么問題,會存在什么問題,并以一種不隱藏業(yè)務(wù)邏輯的方式來使用它們。 爭取表現(xiàn)力! 如今,許多編程語言都是支持流的。例如 Java、Kotlin、JavaScript 等來幫助我們編寫表達(dá)式代碼。流已經(jīng)用 'if' 語句取代了大段的循環(huán)。數(shù)據(jù)流幫助我們以一種聲明式的方式,更具有說明性的操作來進(jìn)行數(shù)據(jù)轉(zhuǎn)換。如果你想找到一個集合中所有小于某個值的元素,循環(huán)迭代集合將沒有意義,只需要通過過濾器操作數(shù)據(jù)流就可以了。 Map、Filter 和 Reduce ,幾乎每一個支持流的語言都支持它。所以,每個人都可以理解你寫的東西,就像每個人都能理解一個循環(huán)或者一個 if 語句一樣。 有這樣的清晰處理邏輯的方式是強大的。首先,你不必測試這個功能,因為它們一定是穩(wěn)定的。而你有沒有注意到第一個例子中的問題?當(dāng)使用函數(shù)式編程的方式,它將變的更加簡單。函數(shù)式編程在這篇文章中,有很多好處,但是我重點介紹它來如何幫助代碼提高可讀性。 第一個例子中,基于流的解決方案如下: 簡單而干凈。很容易就理解它在做什么?,F(xiàn)在,再來看看下面的例子: 你是否期望當(dāng)你調(diào)用這個方法額時候,方法的第二個參數(shù)將會被改變?這個方法是否按照所想的去做?方法名稱是否合適?你真的得到預(yù)期的結(jié)果了嗎? 那么,現(xiàn)在呢? 在這個例子中,返回值是一個新的列表,沒有參數(shù)會受到影響。我們只是讀取參數(shù)并產(chǎn)生一個新的結(jié)果。理解這個方法現(xiàn)在做什么以及如何使用它將變的更容易。這種方法可以很容易的與其它方法組合。 一般而言,組合是流和函數(shù)式編程的最重要的好處之一。組合能使我們能夠在更高級別上進(jìn)行數(shù)據(jù)的轉(zhuǎn)換、過濾等操作,并編寫更具說明性和表達(dá)性的代碼,而不是舊的命令式風(fēng)格。我們寫的代碼表達(dá)了我們想要做的而不是如何完成!這是代碼可讀性的重大改進(jìn)。 把一個大問題分解成多個子問題,解決每一個子問題,然后組合這些解決方案,這將為解決初始問題提供了解決方案。 請注意,Java 8 中的 編寫表達(dá)式代碼不是一件容易的事情。有句 Albert Einstein 說過的名言,'如果你不能簡單的解釋它,你并不是真正的理解它'。所以,當(dāng)我看到抽象層混合的邏輯代碼時,例如與 DAO 交互的 UI 類,能直接與數(shù)據(jù)庫交互,又或者低層次的細(xì)節(jié)在不應(yīng)該被暴露的地方暴露了。我們都知道單一職責(zé)原則的 SOLID 原則,但是關(guān)于這個問題一直受人詬病,因為有些時候很難做職責(zé)的劃分,這部分是它有爭議的關(guān)鍵。在代碼中使用注釋來解釋代碼,并不是一個解決方案,我們將在后面的文章中看到。我相信有人寫的越簡單、越具表達(dá)性的代碼,他或者她對這個問題的理解就越清晰。 擁抱不變性 當(dāng)對象的狀態(tài)發(fā)生變化,而我們沒有注意到它的時候,這真的會讓人困惑。使用返回值可以構(gòu)造一半的對象也是很危險的,特別是當(dāng)我們處理具有多個線程的程序時。共享這些對象真的很難做到正確。另一方面,不可變對象是線程安全的,也是緩存的最佳選擇,因為它們的狀態(tài)不會改變。 但是為什么人們選擇可變對象呢?我相信最有可能的原因是他們認(rèn)為他們會獲得更好的結(jié)果,因為所使用的內(nèi)存會更少,因為這些更改已經(jīng)完成了。而且,讓一個對象的狀態(tài)在其整個生命周期中發(fā)生變化是很自然的。這是我們在 OOP 中學(xué)到的。這些年來,我們一直在寫程序,其中大部分的對象都是可變的。 如今,一個系統(tǒng)的內(nèi)存數(shù)量比幾十年前大了幾個數(shù)量級。我們面臨的真正問題是可擴(kuò)展性。處理器的速度不再像過去幾年那樣告訴的提高了,但是現(xiàn)在我們有了幾十個內(nèi)核的盒子。所以,對于我們的規(guī)模來說,我們需要利用現(xiàn)在的情況。由于我們的程序需要能夠在多個內(nèi)核上運行,所以我們需要以一種安全的方式編寫它們。通過使用可變對象,我們必須處理鎖定以確保其狀態(tài)的一致性。并發(fā)并不是一個小問題要解決。另一方面,由于它們的性質(zhì),不可變對象在多線程和處理器之間共享是固有安全的。而且,不需要同步的事實為創(chuàng)建具有低延遲和高吞吐量的系統(tǒng)提供了機會。因此,不變性是實現(xiàn)可擴(kuò)展性的更安全的選擇。 除了可擴(kuò)展性的好處,不變性使我們的代碼更清潔。在上一節(jié)的第一個示例中,作為參數(shù)傳遞的集合在方法調(diào)用之后發(fā)生了更改。如果收藏是不可改變的,那么這是被禁止的。因此,不變性會促使我們走向更好的解決方案。另外,由于狀態(tài)不可變,閱讀者不必記住他心中的狀態(tài)變化。閱讀者只需要將一個名稱與一個值關(guān)聯(lián)起來,而不記得變量的最新值。
這篇文章更多的是關(guān)于編寫更具可讀性和表達(dá)性的代碼的一般建議。在將來的文章中,我們將討論生產(chǎn)代碼和測試代碼中的氣味。我們也將看到我們?nèi)绾尾拍芡ㄟ^查看我們的測試來在我們的生產(chǎn)代碼中找到可能的設(shè)計問題。 |
|
來自: 慶亮trj21bcn0z > 《編程》