看似簡單但容易忽視的編程常識

這些年寫了很多的代碼、也讀過很多的人寫的代碼,這幾年,寫代碼的機會越來越少,但是每次寫代碼,感覺需要思考的東西越來越多,好的代碼確實難能可貴,在國內業界中,好的軟件不少,但是好的代碼確實有點鳳毛麟角了,寫得出來的人不多,有追求的也不多,看到的好的代碼越來越少。


可能是因為每個人對於好的評判標準不一,程序員中,也不乏文人相輕的較勁,總覺得比人寫的代碼都不夠好,我不想介入這些無謂的爭論,這篇文章中,我將結合我的編碼經驗,探討一下,如何寫出設計優良的代碼,希望作為大家的參考。

好的代碼首先是邏輯正確的

看似簡單但容易忽視的編程常識

如何用編程語言表述正確的代碼邏輯,這個問題好像很少有人單獨拎出來講,因為這個問題的答案很簡單,簡單得你都懶得去思考它,因為你肯定覺得,用編程語言正確的表述代碼邏輯無非就是if 、while 之類的東西,有什麼好探討的,其實我要分享的並不是這些關鍵詞的本身在邏輯中表達的含義,而是這些關鍵詞的背後,編寫程序的過程中,是否真的認真思考過背後的邏輯。我曾不止遇到過很多有年編程經驗的程序員,犯下類似的錯誤,也見過很多年輕的同學,反覆強調糾正後,邏輯上還是會漏洞百出,這幾年,我會經常組織我組裡面的同學對代碼進行走讀,總結這些編碼中的邏輯錯誤,很大一部分也是因為編程邏輯背後的思考是不夠的。所以我要講的,是很簡單的知識,但是往往是最容易忽略的思考點。

我先給大家看一個例子:

這段代碼為的目的是判斷userInfo不為空串的時候couponing,看起來這段代碼非常簡單,判斷上似乎還算比較嚴謹,其實這段代碼只是看到了眼前要做的事情,但是並沒有看到整體邏輯,


if(userInfo != null){

//給用戶派發一個100單位的優惠券

couponing(userInfo,100);

}


為什麼這麼說呢,請看下面幾行代碼,也許會引發最這個簡單問題新的思考。


if(userInfo == null){

//思考嘗試恢復不存在的userInfo情況

userInfo = fetchUserInfo(userId);

}

if(userInfo != null){

couponing(userId);

}else{

//思考userInfo不存在的情況下,是否是符合整體的業務邏輯, 如果整體上不符合業務邏輯,應該立刻異常終端程序。

throw new RuntimeException("userInfo not exist.");

}


這段代碼雖說相比之前的代碼長了一些,但是反映出來的是邏輯思考的嚴謹性,從這兩個例子比較我們可以很明顯的感覺到,第一段代碼的問題,我們看到的只是為了保護是否能做couponing的條件,但是並沒有去思考,條件不滿足的時候,如何去做,是否有能力去恢復這個錯誤,確實無法恢復的時候,我們是否還要在錯誤的道路上越錯越遠呢,這一點非常重要,也很容易忽略,需要在編碼的過程中,進行完整的思考才會意識到這個問題的,如果讓錯誤繼續執行下去,直到程序運行到下一個我們不期望的點,如果下一個不期望的點,代碼上也遵循這個風格,簡單的判斷不為null,就跳過執行,這樣下去,就會有無窮的隱患,代碼整體上看上去,就漏洞百出了。所以從這裡要給大家一個建議:

【要有一顆勇敢的心,程序不要害怕拋出錯誤,越害怕,錯誤越多】

我們應該都知道,錯誤越是早發現越好處理,其實程序在執行過程中也是一樣的,越早發現錯誤,執行中就越容易處理。我一般稱這種代碼為代碼的盲目容錯,看上去這行代碼很健壯,不會報錯,但是不報錯,不能影響錯誤的客觀存在性,錯會還是會存在的,遇到錯誤的時候,我們應該首先想到的是恢復這個錯誤,對容錯問題,是需要進行非常深入很全局的思考才能做的決定,盲目的容錯,只會讓情況變得更加不可控制。

這一小節只是拿一個小例子來說明我們需要有更多的思考,介紹到這裡,我相信大家都理解這些思考的重要性了,但是最關鍵的問題我還沒有給大家說清楚,就是如何保證我們的思考是完整的。我給大家介紹一個我樸素有效的方法,這也是我在做CodeReview中最能發現問題的方法,當然了這邊也可以建議大家去看看這套設計模式,講解如何重構的 教程

(https://pan.baidu.com/share/init?surl=dTCdoq 提取碼 : 29oc),來接著往下看:

【千萬不要忘記else的思考】

每當你要用到一個條件表達式的時候,切記要思考這個條件不成立的情況。 儘可能的不要出現只有if 沒有else的情況,多組條件用 else if 連接使用,最後再加一個else去做大兜底。 其他的條件表達式類似,比如switch case 最後總有一個值得我們深思default。嚴謹的代碼其實就提現在else上面的思考。

看似簡單但容易忽視的編程常識

容易造成思考不足的條件語句

看似簡單但容易忽視的編程常識

條件有兩面性,思考要完整

有效降低邏輯的複雜度

上一節的例子中,肯定會有人覺得這樣寫代碼,是不是覺得太複雜了,已經思考了這些問題,一定要用這麼複雜的方式表達出來嗎?這是另外一方面的問題,我們要讓代碼邏輯變得簡單,這一節中,我嘗試分享一些我如何降低代碼複雜度的方法和經驗。

還是用上面的例子,我嘗試將代碼變得更加簡單,請看下面的代碼,是不是感覺舒服很多。


userInfo = withDefault(userInfo,fetchUserInfo(userId));

Assert.assertNotNull(userInfo,"userInfo not exist.");

couponing(userInfo,100);


這段代碼中,表達了上面所有的邏輯,而且沒有引入分支,其實這裡我想強調的是

【減少分支就是降低複雜度】

我一般的編碼思想是,儘可能的不要用分支處理異常,也不要因為異常引入分支,分支的使用場景最好是業務邏輯所需要的,應該用分支儘可能的表達清楚業務邏輯,而儘量不要用分支去適應異常的處理。這裡進一步又引入了一個被忽略的嘗試。

【不要混淆分支和異常的概念】

這一點看起來很難做到,但是根據我的實際經驗,我們是有辦法做到的,通過優雅的定義和處理異常,是可以比較容易的明確異常和業務分支的區別的。不過在本文中,我還是希望能將減少分之的方法說清楚,關於如何優雅的處理和定義異常,本文先不做過多描述。

我想說的是,一個分支,最好是能表達一層業務的含義,用分支標示是分支的條件以及條件成立或不成立的時候,要做的動作。所以,還是基於上面的例子,我們引入一個業務條件,“當用戶是VIP用戶的時候,我們才能給用戶發放優惠券,否則,我們不發放優惠券”,我們分支代碼標示如下


userInfo = withDefault(userInfo,fetchUserInfo(userId));

Assert.assertNotNull(userInfo,"userInfo not exist.");

if(userInfo.isVip()){

couponing(userInfo,100);

} else {

return;

}


這段代碼正常的表述的業務的含義,注意其中的else,這裡else 進入之後是直接return的,寫上這一句就是上一節中,說明的一樣,保證我們的代碼邏輯是完整的,這一句有很明確的語義,就是表示條件不成立的時候,我們不做,如果不寫的話,其實這部分語義是丟失的或是不明確的。

上面的代碼能正常滿足當前的業務需求,但是業務是複雜的,比如業務上我們有了新的需求,需要對發放優惠券的規則進行調整,調整會後的規則為,增加白名單可以不是VIP也要發優惠券,或者這個用戶的用戶UID是以00結尾,所以這時候,我們條件代碼成了下面這個樣子


if(userInfo.isVip()||inWhiteNameList(userInfo)||

StringUtil.endWith(userInfo.getUserId(),"00"){

couponing(userInfo,100);

} else {

return;

}


這段代碼中,我們邏輯一下就變得複雜了,雖說我們只用了一個if else 表達式,但是這裡的分支複雜度其實是2的3次方,但是我們處理的情況就是兩種,一種是成立,一種是不成立,所以,我們更加關心的是成立或是不成立的情況,而不是所有條件的組合形式,通過觀察,我們發現,所有的邏輯都由“或”進行連接,根據這個特性,其實我們可以提煉出邏輯工具方法,更好的表達我們更加關係的成立或不成立的條件。我們提取一個命名為any的邏輯方法來表述剛才的邏輯,這個方法接收一個不定長的參數,值要有一個為真,則返回為真其他場景,我們也可以自己峰值其他的邏輯方法,比如all。notAll notAny。則代碼修改為


if(any(userInfo.isVip(),inWhiteNameList(userInfo),StringUtil.endWith(userInfo.getUserId(),"00")){

couponing(userInfo,100);

} else {

return;

}


這段代碼有效的減少了代碼的分支數量,注意,這裡僅僅是從分支數量上進行了減少,增加了一點點可讀性,這樣做的好處是,多數情況下,我們關注的業務分支的動作本身,而對於進入這個分組形成的的組合情況做所有討論,所以,這樣做,可以有效的降低分支的數量,減少用例的個數(寫過單元測試的同學都知道,這樣的邏輯要覆蓋有多痛苦)。

這一節中,用了一個看上去有些雞肋的方法去封裝邏輯組合,其實,在現在日常生產中,想辦法去封裝邏輯表達式進行封裝是非常有效果的,這裡只是舉了一個邏輯封裝的例子,還有很多其它場景,比如從一個Map中,根據一組key逐個取值,如果取到值不為null,則放入到另外一個Map中,這裡其實可以寫一個putNotNull的方法來封裝邏輯,這種做法非常有效。所以這一節我想給大家傳遞的一個思想,就是盡你最大的可能,對邏輯表達式進行封裝

【減少分支數量就是減少複雜度】

代碼和業務解耦

上一節的例子中,大家可以很容易看出來,不管邏輯怎麼封裝,代碼是始終不穩定的,其實這裡就引出了我們要強調的一個常識,就是能力要和業務解耦。

如何將能力和業務解耦,我對這個問題的理解是,首先我得把這個能力定義出來,這裡我暫且定義為這個能力為發優惠券(其實定義一個能力是最難做的事情,深的思考,會發現這個問題難到需要重新思考人生,我這裡不拉開篇幅講了,結合這個例子,大家暫且先有一個模糊的理解,後面在慢慢討論能力定義這個大的課題),有了這個能力定義之後,我們根據這個能力定義做一個面向能力的條件判斷,代碼示例如下:


if(canCouponing(userInfo)){

couponing(userInfo,100);

} else {

return;

}


從這幾行代碼中,可以看出,這裡好像已經好了很多,我們將發優惠券的能力和判斷條件canCouponing進行耦合,看上去這段代碼已經穩定了,但是仔細觀察後發現,canCouponing這個方法中依賴了userInfo,這個依賴貌似還是會存在很多問題,因為如果判斷條件超出了userInfo的範疇,則這個地方又會變得難以解決,能力判斷的要素看起來還是不可控的,為了解決這個問題,我們就要用到運行上下文或是領域模型的概念了,用一個運行時的上下文,作為數據信息載體,承載我們業務執行過程中所需要的模型數據,領域模型的發放則是我們對系統能力和業務有了足夠深入理解之後,抽象出來的,能更加準確表述業務屬性和行為的模型定義,在沒有很好的理解和抽象之前,本節中我們還是先用運行上下文這樣相對鬆散的概念來解決這個問題。根據這個思想,我們將代碼進行修改:


if(canCouponing(runtimeContext)){

couponing(userInfo,100);

} else {

return;

}


在上面代碼中,讓runtimeContext中包含userInfo,通過一個更鬆散的對象來傳遞對象,交給canCouponing這個方法處理,這裡也許有人會問,canCouponing這個方法內部還不是一堆邏輯,整體上還是控制不住複雜度。其實這類問題,我們將關鍵的業務點從硬代碼中剝離出來,並且將業務邏輯集中起來進行管理的話,就可以使用規則引擎來處理了。通過規則引擎和專家系統,將這些規則交給業務人員或是運營人員統一進行管理就可以了,而我們的功能性代碼可以做到非常的乾淨和穩定。

也許有另外的人會問,為什麼couponing(userInfo,100);這行代碼中沒有用runtimeContext,而是直接使用的userInfo,在實際編程中,你可能真的需要用到runtimeContext,但是這裡的目的是讓大家理解如何讓業務代碼和能力解耦,關於能力本身這塊如何更好的設計,這一方面的內容也有很多值得我們思考的,本文暫不做過多探討。

來源:http://codebay.cn/post/7865.html 如有侵權,請及時聯繫,謝謝!


分享到:


相關文章: