最近在讀《重構——改善既有代碼的設計》這本書,在 9.4 Remove Control Flag(移除控制標記)這一節,作者提到了“單一入口”和“單一出口”這兩個原則,並對“單一出口”原則批駁了一番,讓我想起了一個遙遠的故事。
那是3年前在H3C實習的日子,開發部門對代碼規范規定略微嚴格,並且有代碼鑒定小組嚴格把關進行代碼檢查。尤其還記得當時對於“單一出口”原則的提倡,比如下面這段代碼:
Function
{
if (condition)
{
return false;
}
return AnotherFunction();
}
會建議為如下的形式:
for loop
{
bRet = FALSE;
if (!condition)
{
bRet = AnotherFunction();
}
return bRet ;
}
在當時,我雖然覺得這樣有道理,卻實際上並沒有明白其中的原因。但是這種做法有一個弊端,也就是如果一個函數當中條件較多時會使得該函數的嵌套層次暴增,這個時候代碼閱讀起來會比較費勁。最近在閱讀編程實踐相關書籍的時候,恰好碰到了對於該單一出口原則的不同的觀點。
來自Boswell與Foucher的反駁
Boswell(Dustin Boswell)和Foucher(Trevor Foucher)是《The Art of Readable Code》一書的兩位作者。在該書第7章的“Returning Early from a Function”小節當中有如下一段話:
Some coders believe that functions should never have multiple return statements. This is nonsense. Returning early from a function is perfectly fine—and often desirable.
很顯然,作者認為刻板的遵守單一出口原則會影響到代碼的可讀性。同時,在“Minimize Nesting”一節,作者還舉了一個使用“returning early from a function”來優化代碼可讀性的例子。
if (user_result == SUCCESS)
{
if (permission_result != SUCCESS)
{
reply.WriteErrors("error reading permissions");
reply.Done();
return;
}
reply.WriteErrors("");
}
else
{
reply.WriteErrors(user_result);
}
reply.Done();
優化后:
if (user_result != SUCCESS)
{
reply.WriteErrors(user_result);
reply.Done();
return;
}
if (permission_result != SUCCESS)
{
reply.WriteErrors(permission_result);
reply.Done();
return;
}
reply.WriteErrors("");
reply.Done();
這個例子主要提倡“在一些罕見情況發生時提前退出”,並沒有直接針對單一出口原則。但也間接地反擊了單一出口原則,因為單一出口原則是絕對不會支持提前return的。其實,我們可以將上面的例子簡單修改一下,使其符合單一出口原則:
if (user_result == SUCCESS)
{
if (permission_result != SUCCESS)
{
reply.WriteErrors("error reading permissions");
}
else // 簡單的加上一個else.
{
reply.WriteErrors("");
}
} else
{
reply.WriteErrors(user_result);
}
reply.Done();
這就是就是對單一出口原則的一次反駁,看起來也不無道理。值得贊賞的是作者這里不僅解釋了他們推崇方法的好處,也提到了為什么單一出口原則得以被提出的原因:保持單一出口原則的一個主要原因是在函數的結尾可以做統一的清理工作,特別對於C語言當中更應該如此。但是,對於現代編程語言當中本身已經提供了類似的機制。
|| Language || Structured idiom for cleanup code ||
--------------------------------------------------------
|| C++ || destructors ||
|| Java, Python || try finally ||
|| Python || with ||
|| C# || using ||
插個小話題:對於作者提到的這個例子,我覺得還可以使用《重構》一書當中的“Consolidate Duplicate Conditional Fragments”進一步優化,如下:
if (user_result != SUCCESS)
{
reply.WriteErrors(user_result);
reply.Done();
return;
}
if (permission_result != SUCCESS)
{
reply.WriteErrors("error reading permissions");
}
else
{
reply.WriteErrors("");
}
reply.Done();
return;
有關“單一出口原則”的更多內容
這里有一篇文章討論了對該原則有更多討論,多數人贊同應用該原則的一些例外情況:
- GuardClause,所謂的“衛語句”,指的是在函數開頭用來判斷一些明顯非法情況並立即退出的語句。比如常見的指針判空操作;
- 函數當中存在有多種返回情況,比如switch/case語句當中的每一個分支執行之后均直接return;
但對該篇文章對應的另一篇文章卻大費筆墨支持單一出口原則,尤其在諸如BASIC、Fortran、C等面向過程的編程語言當中:
I believe a single code block should have one point of entry and one point of exit. A function is a code block, so this holds true for functions. However it also and perhaps more importantly holds true for any other situation where it might be an issue. In languages like BASIC, Fortran, C and Assembly language you can create blocks of code that have multiple points of both entry and exit. Code where this happens is often called 'spaghetti code'. The more points of entry and exit the more difficult it is to debug the code. Eventually, it becomes impossible for any practical purpose.
另外在諸如C++這些現代化編程語言當中也是應該謹慎的:
More modern languages such as C++ offer facilities that insulate programmers from some of the consequences of breaking the single point rule. They do not cause it to stop being a sound rule. They do not turn poor practice into good practice. If you can think of a way that it can go wrong, it will go wrong in the fullness of time. The bizarre rationale offered for dispensing with this rule is born of limited experience and hubris.
在這里,這里和Overflow上面的討論1和討論2有更多與此相關的內容,不妨移步去湊湊熱鬧。
自己的觀點
在寫這篇筆記的前后,我的觀點是有一些細微的變化的。
從懵懵懂懂繼承單一出口原則編程手法之后到不久之前,自己一直如此實踐,也覺得這樣寫是一種自然的寫法。當然,這主要得益於沒有編寫過邏輯太過龐雜的函數以至於讓if...else語句嵌套出富麗堂皇的N層樓閣。所以,在閱讀到《編寫可讀代碼的藝術》和《重構——改善既有代碼的設計》的相關章節之時,突然覺得自己原來一直所堅持的是錯誤的。這也促使自己決定將此記錄下來,備忘之余說不定也能分享給其他人。
在閱讀了不少網絡上的觀點之后,一顆激動的心反而冷靜下來了不少。仔細想想,兩種做法都具有可取之處,也不一定得非此即彼,一定要占到某個陣營當中去。比如,在筆記開頭的那個例子當中可以不用遵守單一出口原則,代碼閱讀起來會更簡潔。另外,不論是在面向過程,還是面向對象的程序設計語言之中,謹慎的遵守單一出口原則如果可以讓程序的可讀性更好,那就好好使用它。