
所屬文章系列:尋找塵封的銀彈:自創(chuàng)實現(xiàn)模式
今天要寫第一篇自創(chuàng)實現(xiàn)模式,也就是我自己獨家創(chuàng)造的一個實現(xiàn)模式。
先解釋一下什么是實現(xiàn)模式,這個詞是Kent Beck《實現(xiàn)模式》的書名,意思就是在代碼的最底層的模式,即類內(nèi)部及函數(shù)內(nèi)部的模式,而設(shè)計模式是多個類之間的模式。
說是獨家創(chuàng)造,其實很可能早已有人提出來過,或者實踐過,只是我不知道而已,就像設(shè)計模式的最初作者也并非是《設(shè)計模式》書中的四個作者一樣,我只是一個模式的總結(jié)者而已。無論如何,對程序員有幫助,才是最重要的,也是我的初衷。
【動機(jī)】
先看一段看起來邏輯清晰,但有漏洞的代碼:
void DoAdjust(ClassA **a, ClassB **b) {
? ??if ((*a) == NULL)?{
? ? ? ??if ((*b)->next?!= NULL)?{
? ? ? ? ? ??*a = *b;
? ??? ? ? ? *b = (*b)->next;
? ??? ??}
? ??}
}
注:代碼中的雙指針(**)只是為了在函數(shù)體內(nèi)給外部的指針賦值,本質(zhì)是輸出參數(shù),方式是解引用,即*a。
表面上看起來,這段代碼邏輯清晰,但只是看似清晰而已,實際上編寫這段代碼的人并沒有搞清楚這兩個變量在所有執(zhí)行路徑到底該如何賦值。
這段代碼漏掉了一種情況:當(dāng)(*a) != NULL時,b也應(yīng)該根據(jù)(*b)->next來決定取值,具體的取值應(yīng)該有特定的業(yè)務(wù)邏輯。
當(dāng)我根據(jù)需求編寫了較為完備的測試用例之后,我才發(fā)現(xiàn)了這個Bug。經(jīng)過分析,在這段代碼中,確實應(yīng)該有這種業(yè)務(wù)邏輯,但是這段代碼卻使用了默認(rèn)的“不作為”行為,導(dǎo)致錯誤發(fā)生!
于是,我們加上了一些代碼來補上這個漏洞:
void DoAdjust(ClassA **a, ClassB **b) {
? ??if ((*a) == NULL)?{
? ? ? ??if ((*b)->next?!= NULL)?{
? ? ? ? ? ??*a = *b;
? ??? ? ? ? *b = (*b)->next;
? ??? ??}
? ??}?else {
? ? ? ??if ((*b)->next?!= NULL)?{
? ??? ? ? ? *b = (*b)->next;
? ??? ??}
? ??}
}
漏洞是補上了,但再觀察這段代碼,總是感覺哪里不對,會不會還有漏洞?這是因為我們無法一眼看出問題,必須用白盒的方式枚舉所有的情況。
由于這段代碼比較簡單,只有三個條件:*a、*b、(*b)->next,需要處理的情況數(shù)= 2(*a為空或非空) * 2(*b為空或非空) * 2((*b)->next為空或非空) = 8,所以枚舉起來并不太難。
但是,當(dāng)條件的數(shù)目猛增到10個時,它們的變化就會有2的10次方個,也就是1024個,那么枚舉的方式就顯得很傻,而且容易出錯。不過,程序員總是想追求好方法,讓自己在檢查代碼漏洞的時候可以更加“懶惰”一點。
有沒有一種方法讓這段代碼看上去一目了然,一眼就能看出有沒有漏洞?
【典型代碼】
答案當(dāng)然是有。用的方法是“以結(jié)果驅(qū)動代替條件驅(qū)動”。
先看修改后的代碼,下一小節(jié)再給你解釋:
void DoAdjust(ClassA **a, ClassB **b) {
? ?//為a賦值
? ??if ((*a) != NULL) {
? ??? ??*a = *a;
? ??} else if ((*b)->next)
?? ?? ??*a = *b;
? ??} else?{
? ??? ??*a = NULL;
? ??}
? ?//為b賦值
? ??if ((*b)->next?!= NULL)?{
? ??? ??*b = *b->next;
? ??} else {
? ??? ??*b = *b;
? ??}
}
【優(yōu)劣對比】
使用該模式后的代碼,看起來有點累贅,但你稍稍看仔細(xì)一點,就能完全懂得代碼的所有意圖和所有變化,做到了然于心。
為什么這樣說?
這個函數(shù)要解決的根本問題是為*a和*b返回值,而使用該模式的代碼恰恰是從這個為*a和*b賦值的角度出發(fā)的,我們一下子就能看出*a和*b是不是在退出的時候被賦過值了。這就是“以結(jié)果驅(qū)動代替條件驅(qū)動”這個實現(xiàn)模式中的“結(jié)果驅(qū)動”。
反觀未使用該模式的代碼,它是從條件的角度出發(fā),條件包括*a、*b和(*b)->next,我們只看到了眼花繚亂的各種條件組合,卻容易忽略掉最終要給每個返回變量賦值。這就是“以結(jié)果驅(qū)動代替條件驅(qū)動”這個實現(xiàn)模式中的“條件驅(qū)動”。
【思維進(jìn)階(一):單一路徑思維、顯式思維】
該模式體現(xiàn)出的是兩種思維:單一路徑思維和顯式思維。
單一路徑思維:函數(shù)必須確保在函數(shù)內(nèi)為每個輸出變量賦值的地方只有一處,而不依賴于函數(shù)內(nèi)其他路徑中的賦值代碼。如“典型代碼”小節(jié)中的代碼:
? ?//為a賦值
? ??if ((*a) != NULL) {
? ??? ??*a = *a;
? ??} else if ((*b)->next)
?? ?? ??*a = *b;
? ??} else?{
? ??? ??*a = NULL;
? ??}
顯式思維:在這一處賦值的代碼中,所有if/else路徑都為那個輸出變量賦值,而且必須包含else。代碼示例也是如上這段代碼。
“單一路徑思維”中還提到了“依賴于函數(shù)內(nèi)其他路徑中的賦值”,這是反面教材,代碼示例如下:
void DoAdjust(ClassA **a, ClassB **b) {
? ??if (flag == TRUE) {
? ??? ?*a = NULL; //此處為“其他路徑”
? ??}
? ??if ((*a) != NULL) {
? ??? ??*a = *a;
? ??} else if ((*b)->next)
?? ?? ??*a = *b;
? ??} else
? ??? ??*a = NULL;
? ??}
? ??...
}
這種代碼就會讓人思維混亂,理不清頭緒。
【思維進(jìn)階(二):多個條件的優(yōu)先級】
這個模式會迫使你思考:
到底是在什么特定情況下,該給什么值。而不是遇到“條件1”先給賦一個值,再遇到“條件2”再給賦一個值,就像剛舉的這個反面教材代碼那樣。當(dāng)我們有本文這種模式的思維時,就會豁然開朗,一切邏輯都是那么清晰可見!
再看這種“營養(yǎng)不良”的代碼產(chǎn)生的原因:一般是由于多個人先后修改代碼產(chǎn)生的。后面修改代碼的人不去看已有代碼,他也不知道兩個條件的優(yōu)先級是什么,他只是加上自己的邏輯就以為萬事大吉。這種情況在真實的產(chǎn)品代碼里非常常見,它是一大類Bug的根源,卻又藏得很深。
【結(jié)束語】
當(dāng)我們了解了本文所講的“以結(jié)果驅(qū)動代替條件驅(qū)動”這種實現(xiàn)模式后,即便不能按照這種模式修改“由堆積如山的條件”組成的產(chǎn)品代碼,也能通過本文的思路找到Bug的根源,從而找到合適的解決方案。
作于2018-5-23
------------------------------
心定時刻:夫道不欲雜,雜則多,多則擾,擾則憂,憂而不救。
------------------------------