有些人吵著鬧著要讓程序“模塊化”,結果他們的做法是把代碼分部到多個文件和目錄里面,然后把這些目錄或者文件叫做“module”。他們甚至把這些目錄分放在不同的VCS repo里面。結果這樣的作法并沒有帶來合作的流暢,而是帶來了許多的麻煩。這是因為他們其實并不理解什么叫做“模塊”,膚淺的把代碼切割開來,分放在不同的位置,其實非但不能達到模塊化的目的,而且制造了不必要的麻煩。
真正的模塊化,并不是文本意義上的,而是邏輯意義上的。一個模塊應該像一個電路芯片,它有定義良好的輸入和輸出。實際上一種很好的模塊化方法早已經存在,它的名字叫做“函數”。每一個函數都有明確的輸入(參數)和輸出(返回值),同一個文件里可以包含多個函數,所以你其實根本不需要把代碼分開在多個文件或者目錄里面,同樣可以完成代碼的模塊化。我可以把代碼全都寫在同一個文件里,卻仍然是非常模塊化的代碼。
想要達到很好的模塊化,你需要做到以下幾點:
* 避免寫太長的函數。如果發現函數太大了,就應該把它拆分成幾個更小的。通常我寫的函數長度都不超過40行。對比一下,一般筆記本電腦屏幕所能容納的代碼行數是50行。我可以一目了然的看見一個40行的函數,而不需要滾屏。只有40行而不是50行的原因是,我的眼球不轉的話,最大的視角只看得到40行代碼。
如果我看代碼不轉眼球的話,我就能把整片代碼完整的映射到我的視覺神經里,這樣就算忽然閉上眼睛,我也能看得見這段代碼。我發現閉上眼睛的時候,大腦能夠更加有效地處理代碼,你能想象這段代碼可以變成什么其它的形狀。40行并不是一個很大的限制,因為函數里面比較復雜的部分,往往早就被我提取出去,做成了更小的函數,然后從原來的函數里面調用。
* 制造小的工具函數。如果你仔細觀察代碼,就會發現其實里面有很多的重復。這些常用的代碼,不管它有多短,提取出去做成函數,都可能是會有好處的。有些幫助函數也許就只有兩行,然而它們卻能大大簡化主要函數里面的邏輯。
有些人不喜歡使用小的函數,因為他們想避免函數調用的開銷,結果他們寫出幾百行之大的函數。這是一種過時的觀念。現代的編譯器都能自動的把小的函數內聯(inline)到調用它的地方,所以根本不產生函數調用,也就不會產生任何多余的開銷。
同樣的一些人,也愛使用宏(macro)來代替小函數,這也是一種過時的觀念。在早期的C語言編譯器里,只有宏是靜態“內聯”的,所以他們使用宏,其實是為了達到內聯的目的。然而能否內聯,其實并不是宏與函數的根本區別。宏與函數有著巨大的區別(這個我以后再講),應該盡量避免使用宏。為了內聯而使用宏,其實是濫用了宏,這會引起各種各樣的麻煩,比如使程序難以理解,難以調試,容易出錯等等。
* 每個函數只做一件簡單的事情。有些人喜歡制造一些“通用”的函數,既可以做這個又可以做那個,它的內部依據某些變量和條件,來“選擇”這個函數所要做的事情。比如,你也許寫出這樣的函數:
~~~
void foo() {
if (getOS().equals("MacOS")) {
a();
} else {
b();
}
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
~~~
寫這個函數的人,根據系統是否為“MacOS”來做不同的事情。你可以看出這個函數里,其實只有`c()`是兩種系統共有的,而其它的`a()`,?`b()`,?`d()`,?`e()`都屬于不同的分支。
這種“復用”其實是有害的。如果一個函數可能做兩種事情,它們之間共同點少于它們的不同點,那你最好就寫兩個不同的函數,否則這個函數的邏輯就不會很清晰,容易出現錯誤。其實,上面這個函數可以改寫成兩個函數:
~~~
void fooMacOS() {
a();
c();
d();
}
~~~
和
~~~
void fooOther() {
b();
c();
e();
}
~~~
如果你發現兩件事情大部分內容相同,只有少數不同,多半時候你可以把相同的部分提取出去,做成一個輔助函數。比如,如果你有個函數是這樣:
~~~
void foo() {
a();
b()
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
~~~
其中`a()`,`b()`,`c()`都是一樣的,只有`d()`和`e()`根據系統有所不同。那么你可以把`a()`,`b()`,`c()`提取出去:
~~~
void preFoo() {
a();
b()
c();
~~~
然后制造兩個函數:
~~~
void fooMacOS() {
preFoo();
d();
}
~~~
和
~~~
void fooOther() {
preFoo();
e();
}
~~~
這樣一來,我們既共享了代碼,又做到了每個函數只做一件簡單的事情。這樣的代碼,邏輯就更加清晰。