這些年寫了很多的代碼、也讀過很多的人寫的代碼,這幾年,寫代碼的機會越來越少,但是每次寫代碼,感覺需要思考的東西越來越多,好的代碼確實難能可貴,在國內(nèi)業(yè)界中,好的軟件不少,但是好的代碼確實有點鳳毛麟角了,寫得出來的人不多,有追求的也不多,看到的好的代碼越來越少。 可能是因為每個人對于好的評判標(biāo)準(zhǔn)不一,程序員中,也不乏文人相輕的較勁,總覺得比人寫的代碼都不夠好,我不想介入這些無謂的爭論,這篇文章中,我將結(jié)合我的編碼經(jīng)驗,探討一下,如何寫出設(shè)計優(yōu)良的代碼,希望作為大家的參考。 好的代碼首先是邏輯正確的 如何用編程語言表述正確的代碼邏輯,這個問題好像很少有人單獨拎出來講,因為這個問題的答案很簡單,簡單得你都懶得去思考它,因為你肯定覺得,用編程語言正確的表述代碼邏輯無非就是if 、while 之類的東西,有什么好探討的,其實我要分享的并不是這些關(guān)鍵詞的本身在邏輯中表達(dá)的含義,而是這些關(guān)鍵詞的背后,編寫程序的過程中,是否真的認(rèn)真思考過背后的邏輯。 我曾不止一次遇到有多年編程經(jīng)驗的程序員,犯下類似的錯誤,也見過很多年輕的同學(xué),反復(fù)強調(diào)糾正后,邏輯上還是會漏洞百出,這幾年,我會經(jīng)常組織我組里面的同學(xué)對代碼進(jìn)行走讀,總結(jié)這些編碼中的邏輯錯誤,很大一部分也是因為編程邏輯背后的思考是不夠的。所以我要講的,是很簡單的知識,但是往往是最容易忽略的思考點。 我先給大家看一個例子: 這段代碼為的目的是判斷userInfo不為空串的時候couponing,看起來這段代碼非常簡單,判斷上似乎還算比較嚴(yán)謹(jǐn),其實這段代碼只是看到了眼前要做的事情,但是并沒有看到整體邏輯,為什么這么說呢,請看下面幾行代碼,也許會引發(fā)最這個簡單問題新的思考。 這段代碼雖說相比之前的代碼長了一些,但是反映出來的是邏輯思考的嚴(yán)謹(jǐn)性,從這兩個例子比較我們可以很明顯的感覺到,第一段代碼的問題,我們看到的只是為了保護(hù)是否能做couponing的條件,但是并沒有去思考,條件不滿足的時候,如何去做,是否有能力去恢復(fù)這個錯誤,確實無法恢復(fù)的時候,我們是否還要在錯誤的道路上越錯越遠(yuǎn)呢,這一點非常重要,也很容易忽略,需要在編碼的過程中,進(jìn)行完整的思考才會意識到這個問題的,如果讓錯誤繼續(xù)執(zhí)行下去,直到程序運行到下一個我們不期望的點,如果下一個不期望的點,代碼上也遵循這個風(fēng)格,簡單的判斷不為null,就跳過執(zhí)行,這樣下去,就會有無窮的隱患,代碼整體上看上去,就漏洞百出了。所以從這里要給大家一個建議: 【要有一顆勇敢的心,程序不要害怕拋出錯誤,越害怕,錯誤越多】 我們應(yīng)該都知道,錯誤越是早發(fā)現(xiàn)越好處理,其實程序在執(zhí)行過程中也是一樣的,越早發(fā)現(xiàn)錯誤,執(zhí)行中就越容易處理。我一般稱這種代碼為代碼的盲目容錯,看上去這行代碼很健壯,不會報錯,但是不報錯,不能影響錯誤的客觀存在性,錯會還是會存在的,遇到錯誤的時候,我們應(yīng)該首先想到的是恢復(fù)這個錯誤,對容錯問題,是需要進(jìn)行非常深入很全局的思考才能做的決定,盲目的容錯,只會讓情況變得更加不可控制。
每當(dāng)你要用到一個條件表達(dá)式的時候,切記要思考這個條件不成立的情況。 盡可能的不要出現(xiàn)只有if 沒有else的情況,多組條件用 else if 連接使用,最后再加一個else去做大兜底。 其他的條件表達(dá)式類似,比如switch case 最后總有一個值得我們深思default。嚴(yán)謹(jǐn)?shù)拇a其實就提現(xiàn)在else上面的思考。 容易造成思考不足的條件語句 【條件有兩面性,思考要完整】 有效降低邏輯的復(fù)雜度 上一節(jié)的例子中,肯定會有人覺得這樣寫代碼,是不是覺得太復(fù)雜了,已經(jīng)思考了這些問題,一定要用這么復(fù)雜的方式表達(dá)出來嗎?這是另外一方面的問題,我們要讓代碼邏輯變得簡單,這一節(jié)中,我嘗試分享一些我如何降低代碼復(fù)雜度的方法和經(jīng)驗。 還是用上面的例子,我嘗試將代碼變得更加簡單,請看下面的代碼,是不是感覺舒服很多。 這段代碼中,表達(dá)了上面所有的邏輯,而且沒有引入分支,其實這里我想強調(diào)的是 【減少分支就是降低復(fù)雜度】 我一般的編碼思想是,盡可能的不要用分支處理異常,也不要因為異常引入分支,分支的使用場景最好是業(yè)務(wù)邏輯所需要的,應(yīng)該用分支盡可能的表達(dá)清楚業(yè)務(wù)邏輯,而盡量不要用分支去適應(yīng)異常的處理。這里進(jìn)一步又引入了一個被忽略的嘗試。 【不要混淆分支和異常的概念】 這一點看起來很難做到,但是根據(jù)我的實際經(jīng)驗,我們是有辦法做到的,通過優(yōu)雅的定義和處理異常,是可以比較容易的明確異常和業(yè)務(wù)分支的區(qū)別的。不過在本味中,我還是希望能將減少分之的方法說清楚,關(guān)于如何優(yōu)雅的處理和定義異常,本文先不做過多描述。 我想說的是,一個分支,最好是能表達(dá)一層業(yè)務(wù)的含義,用分支標(biāo)示是分支的條件以及條件成立或不成立的時候,要做的動作。所以,還是基于上面的例子,我們引入一個業(yè)務(wù)條件,“當(dāng)用戶是VIP用戶的時候,我們才能給用戶發(fā)放優(yōu)惠券,否則,我們不發(fā)放優(yōu)惠券”,我們分支代碼標(biāo)示如下: 這段代碼正常的表述的業(yè)務(wù)的含義,注意其中的else,這里else 進(jìn)入之后是直接return的,寫上這一句就是上一節(jié)中,說明的一樣,保證我們的代碼邏輯是完整的,這一句有很明確的語義,就是表示條件不成立的時候,我們不做,如果不寫的話,其實這部分語義是丟失的或是不明確的。 上面的代碼能正常滿足當(dāng)前的業(yè)務(wù)需求,但是業(yè)務(wù)是復(fù)雜的,比如業(yè)務(wù)上我們有了新的需求,需要對發(fā)放優(yōu)惠券的規(guī)則進(jìn)行調(diào)整,調(diào)整會后的規(guī)則為,增加白名單可以不是VIP也要發(fā)優(yōu)惠券,或者這個用戶的用戶UID是以00結(jié)尾,所以這時候,我們條件代碼成了下面這個樣子 這段代碼中,我們邏輯一下就變得復(fù)雜了,雖說我們只用了一個if else 表達(dá)式,但是這里的分支復(fù)雜度其實是2的3次方,但是我們處理的情況就是兩種,一種是成立,一種是不成立,所以,我們更加關(guān)心的是成立或是不成立的情況,而不是所有條件的組合形式,通過觀察,我們發(fā)現(xiàn),所有的邏輯都是由“或”進(jìn)行連接,根據(jù)這個特性,其實我們可以提煉出邏輯工具方法,更好的表達(dá)我們更加關(guān)系的成立或不成立的條件。我們提取一個命名為any的邏輯方法來表述剛才的邏輯,這個方法接收一個不定長的參數(shù),值要有一個為真,則返回為真。其他場景,我們也可以自己峰值其他的邏輯方法,比如all、notAll notAny。 則代碼修改為 ![]() 這段代碼有效的減少了代碼的分支數(shù)量,注意,這里僅僅是從分支數(shù)量上進(jìn)行了減少,增加了一點點可讀性,這樣做的好處是,多數(shù)情況下,我們關(guān)注的業(yè)務(wù)分支的動作本身,而對于進(jìn)入這個分組形成的的組合情況做所有討論,所以,這樣做,可以有效的降低分支的數(shù)量,減少用例的個數(shù)(寫過單元測試的同學(xué)都知道,這樣的邏輯要覆蓋有多痛苦)。 這一節(jié)中,用了一個看上去有些雞肋的方法去封裝邏輯組合,其實,在現(xiàn)在日常生產(chǎn)中,想辦法去封裝邏輯表達(dá)式進(jìn)行封裝是非常有效果的,這里只是舉了一個邏輯封裝的例子,還有很多其它場景,比如從一個Map中,根據(jù)一組key逐個取值,如果取到值不為null,則放入到另外一個Map中,這里其實可以寫一個putNotNull的方法來封裝邏輯,這種做法非常有效。所以這一節(jié)我想給大家傳遞的一個思想,就是盡你最大的可能,對邏輯表達(dá)式進(jìn)行封裝
代碼和業(yè)務(wù)解耦 上一節(jié)的例子中,大家可以很容易看出來,不管邏輯怎么封裝,代碼是始終不穩(wěn)定的,其實這里就引出了我們要強調(diào)的一個常識,就是能力要和業(yè)務(wù)解耦。 如何將能力和業(yè)務(wù)解耦,我對這個問題的理解是,首先我得把這個能力定義出來,這里我暫且定義為這個能力為發(fā)優(yōu)惠券(其實定義一個能力是最難做的事情,深的思考,會發(fā)現(xiàn)這個問題難到需要重新思考人生,我這里不拉開篇幅講了,結(jié)合這個例子,大家暫且先有一個模糊的理解,后面在慢慢討論能力定義這個大的課題),有了這個能力定義之后,我們根據(jù)這個能力定義做一個面向能力的條件判斷,代碼示例如下: ![]() 從這幾行代碼中,可以看出,這里好像已經(jīng)好了很多,我們將發(fā)優(yōu)惠券的能力和判斷條件canCouponing進(jìn)行耦合,看上去這段代碼已經(jīng)穩(wěn)定了,但是仔細(xì)觀察后發(fā)現(xiàn),canCouponing這個方法中依賴了userInfo,這個依賴貌似還是會存在很多問題,因為如果判斷條件超出了userInfo的范疇,則這個地方又會變得難以解決,能力判斷的要素看起來還是不可控的,為了解決這個問題,我們就要用到運行上下文或是領(lǐng)域模型的概念了,用一個運行時的上下文,作為數(shù)據(jù)信息載體,承載我們業(yè)務(wù)執(zhí)行過程中所需要的模型數(shù)據(jù),領(lǐng)域模型的發(fā)放則是我們對系統(tǒng)能力和業(yè)務(wù)有了足夠深入理解之后,抽象出來的,能更加準(zhǔn)確表述業(yè)務(wù)屬性和行為的模型定義,在沒有很好的理解和抽象之前,本節(jié)中我們還是先用運行上下文這樣相對松散的概念來解決這個問題。根據(jù)這個思想,我們將代碼進(jìn)行修改: 在上面代碼中,讓runtimeContext中包含userInfo,通過一個更松散的對象來傳遞對象,交給canCouponing這個方法處理,這里也許有人會問,canCouponing這個方法內(nèi)部還不是一堆邏輯,整體上還是控制不住復(fù)雜度。其實這類問題,我們將關(guān)鍵的業(yè)務(wù)點從硬代碼中剝離出來,并且將業(yè)務(wù)邏輯集中起來進(jìn)行管理的話,就可以使用規(guī)則引擎來處理了。通過規(guī)則引擎和專家系統(tǒng),將這些規(guī)則交給業(yè)務(wù)人員或是運營人員統(tǒng)一進(jìn)行管理就可以了,而我們的功能性代碼可以做到非常的干凈和穩(wěn)定。 也許有另外的人會問,為什么couponing(userInfo,100);這行代碼中沒有用runtimeContext,而是直接使用的userInfo,在實際編程中,你可能真的需要用到runtimeContext,但是這里的目的是讓大家理解如何讓業(yè)務(wù)代碼和能力解耦,關(guān)于能力本身這塊如何更好的設(shè)計,這一方面的內(nèi)容也有很多值得我們思考的,本文暫不做過多探討。 【思考能力的定義,用代碼描述能力,將業(yè)務(wù)從代碼中抽出來,交給規(guī)則引擎或是專家系統(tǒng)處理】 |
|