前言
以下將用一個小程序來探討一些大道理, 這些大道理包括可擴展性, 抽象與封裝, 可維護性, 健壯性, 團隊合作, 工具的利用, 可測試性, 自頂向下, 分而治之, 分層, 可讀性, 模塊化, 松耦合, MVC, 領域模型, 甚至對稱性, 香農的信息論等等.
為什么不用大程序來說大道理呢?
因為大程序太大了, 代碼一端上來, 讀者就暈菜了, 看不過來甚至壓根不想去看, 這樣說理就很抽象了, 效果反而不好.
小程序中也能說出大道理來嗎?
我們有句話, 叫"以小見大", 我們又常常有種說法, 叫:
麻雀雖小, 五毒俱全. (咦? 好像應該是五臟俱全…總之你明白我的意思就好了. )
所以呢, 小程序也是可以來說大道理的, 而且小程序又有短小的特點, 大家看得也沒那么累, 也很快能看懂. 畢竟那種代碼, 叫什么來著, “意大利面條式的代碼”, 大家在實際的開發中, 已經見得太多了.
意大利面(spaghetti), 翻譯過來好像叫"通心粉", 非常長的一條條, 彼此纏纏繞繞的, 所以"意大利面條式的代碼"就是又長又繞, 讓人非常頭痛的那種代碼.
按我們的習慣, 也許叫它"裹腳布式的代碼"大家覺得更熟悉, 更形象一點, 也正好符合"又長又臭"的特點.
啊, 說"又長又臭"可能有點刻薄了, 畢竟大家都可能寫過這樣的代碼(本人就寫過好多), 即便現在不會再寫這樣的代碼, 想當年應該也是寫過的, 除非你從一開始覺悟通天, 那我就無話可說了.
這種代碼我們在工作中見得太多了, 所以這里就不再弄出來考驗大家的毅力了, 閑話少提, 讓我們看個簡單的例子.
我們的例子
就是要打印出如下的一個三角形圖案:
****
*****
當然了, 這只是以三行為一個示例, 我們的程序應該接受任意的正整數, 比如, 給一個 5, 就要能打出 5 行的類似的三角形來. 讓我們來看看如何寫出這樣一個程序, 并在這個過程中借此兜售我們的大道理.
玩具式的代碼
我知道很多"數學帝"可能一眼就被圖案中的規律吸引過去了, 他們很快就指出星號是等差數列, 然后很快就弄出了計算每行前面要縮進多少個空格的公式, 然后呢, 一層循環, 二層循環, blablablah…然后最里面幾條優雅而性感的 print 語句, 搞掂!一種智商上的優越感油然而生, 接著他們可能就要問:
這么簡單的東西, 你也好意思拿出來講?
下面是這樣的一個代碼, 能夠完全實現以上要求(只演示了 3 行的情況):
public static void main(String[] args) {int i = 3;for (int j = 0; j < i; j++) {for (int k = 0; k < i - j; k++) {System.out.print(" ");}for (int z = 0; z < 2*j+1; z++) {System.out.print("*");}System.out.println();}
}
這里用的是 java 語言來演示, 包括以下的. 我相信像 java 這樣爛大街的語言, 即使你沒這個背景看懂也不是難事, 在代碼中也不會用到什么高深的特性. (這一點皆因我的能力有限所導致, 而不是想裝逼的意愿所能決定的~)
怎么說呢, 我們不要以上那種"玩具式"的代碼(toy program), 我們要的是生產級(production, 生產環境)的代碼.
生產級的代碼
讓我們來看看如何寫出這樣的代碼.
可擴展性(Extensibility)
首先呢, print 語句是絕對要避免的. 你要明白, print 語句寫得太死, 而需求是不斷在變化的, 有句話是怎么說的?
唯一不變的就是變化本身.
客戶今天跟你說的是要 print 這個圖案, 你要是按著客戶怎么說, 你就怎么做, 你可就慘了.
客戶哪一天突然又會說, 再加點特性, 要能輸出到文件;哪一天又說, 再加點 web service, 能供其它程序調用.
讓我們多留點心眼, 代碼如下:
public void printPattern(int lineCount) {String pattern = getPattern(lineCount);System.out.print(pattern);
}
我們先借助 getPattern
方法拿到要打印的內容, 這樣, 如果哪天要輸出到文件, 哪天要供 web service 調用, 我們都可以把這個 getPattern 方法提供出去.
我們只要多抽象出那么一層來, 就會給我們帶來很多方便.
抽象與封裝(Abstraction & Encapsulation)
抽象與封裝同時也是很多其它特性的基礎, 在后面我們還會不斷說到這一主題.
getPattern
就是一個抽象, 是對一系列動作的一個封裝.
可能有人會比較教條地認為抽象與封裝只能在類層次中進行, 這常常導致在類的內部缺少必要的抽象層次, 常常是一大件事情在一個方法里完成, 方法巨大巨長無比, 這樣的所謂面向對象編程不過是虛有其表, 其模塊性甚至還比不上那些用面向過程語言寫就的代碼.
在 printPattern
層面, 我們不需要知道 getPattern 的細節, 我們只需要傳入所需參數及定義好需要的返回值即可.
大道理: 定義好輸入與輸出, 描述清楚想要做的事, 先不用去管細節.
然后呢, 我們是不是需要手動去把這個方法寫出來呢?
利用好你的工具(Tools)
你不用手動去做這些, 以 eclipse 為例, 只要把光標定位到錯誤的地方(可以按Ctrl+“.”(點)快速定位), 然后按下"Ctrl+1", 然后選擇"Create Method"即可:
工具將根據傳入參數及返回值自動為我們生成方法, 結果如下:
只要輸入與輸出定義清楚了, 工具就能自動幫我們生成方法定義, 這里默認它是 private
的, 我們可以把它改成 public
.
這里說的是 Eclipse 這個 IDE, 其它的我相信也會有類似的功能. 如果你偏好輕量級的文本編輯器, 那我就不敢說也一定有這些功能了.
利用好任務標識(Task Tags)
我們可以看到, 生成的代碼里有個 TODO, 顯示出了特別的顏色, 這是個任務標識.
類似的標識還有 FIXME, XXX, 甚至你還可以自定義標識.
打開 eclipse 的菜單-- windows–preferences, 在過濾框中輸入"task tag":
這些有什么用呢? 我們可以看下, 在編輯器的左右側, 都有顯著的標志提示有個任務標識存在;在 Markers 視圖里, 有列舉出這些標識:
在代碼質量分析工具 sonar 中, 它也會追蹤這些標識. 下圖是我在 sonar.oschina.net 上的一個項目的截圖:
這些有什么用呢? 我們在寫代碼中, 寫到一半, 很可能被某些難題卡住了, 為了不中斷正常的流程, 我們先用個 TODO 來標識, 然后就可以繼續地把一些簡單的問題先處理完, 再回過頭來對付這些.
又或者像現在這樣, 我們生成了出來了這個方法, 工具為我們自動加了入"TODO"的標識, 畢竟方法的主體還沒有, 可不巧的是, 現在到了下班時間了, 然后呢, 我們就可以存盤并提交到 svn 或者 git 上去了. 有人可能要說: "啊? 不是吧, 你的代碼都沒寫完你怎么就提交了? "
沒關系, 我們已經標識好了 TODO, 所以它會提醒我們還有工作是沒做完的. 另外我們為何如此著急提交呢? 因為我們并不是在單打獨斗:
團隊合作(Teamwork)
我們前面說了, 我們可能還要做輸出圖案到文件的需求, 很可能你有個同事哥們, 他就正做著這個模塊, 而他現在呢, 就在等著你這個 getPattern
方法. 你提交了, 他就可以繼續寫他的代碼了:
package org.jcc.core.demo;public class PatternFile {private Pattern pattern;public PatternFile(Pattern pattern) {this.pattern = pattern;}public void generatePatternFile(int lineCount) {String content = pattern.getPattern(lineCount);saveInFile(content);}private void saveInFile(String content) {// TODO Auto-generated method stub//System.out.println(content);}
}
可以看到, 他的類依賴你的類, 在他的方法 generatePatternFile
里還調用了 getPattern
方法, 你沒實現, 那又怎樣呢? 接口好了就行了!
面向接口編程(Interface)
有人可能比較死板, 比較教條主義, 以為呢, 說到接口就一定要弄個 interface, 其實呢, 我們這個方法 getPattern
就是一個承諾, 一個約定, 一個協議, 也是一個廣義上的接口.
有人可能要問, 你方法細節還沒有實現, 他怎么測試? 別擔心, 辦法會有的:
利用 Mockito 來測試
代碼如下:
@Test
public void testGeneratePatternFile() {// 用mockito來模擬接口的行為, 為此我們手動構建一個三行的圖案Pattern pattern = Mockito.mock(Pattern.class);String mockContent = " *" + System.lineSeparator() + " ***" + System.lineSeparator() + "*****" + System.lineSeparator();// 當調用getPattern方法時, 就返回這里定義好的內容. Mockito.when(pattern.getPattern(3)).thenReturn(mockContent);// 測試generatePatternFile方法, 在它的里面將會調用getPattern方法PatternFile pf = new PatternFile(pattern);pf.generatePatternFile(3);// TODO 斷言文件存在并且文件中的內容與mockContent一致// assert that file is exists and content in file is equals the mock content
}
以上我們用一個 mock
對象以及 when
, thenReturn
來主動模擬一個尚未實現的方法.
你也許對 Mock 之類的技術還不太了解, 但這些詞表達的意思我想大家都不難明白. Mock 的更詳細介紹請自行百度之.
借助 Mockito, 這個哥們就可以這樣寫好他的代碼, 并完成他的測試了, 然后可以提交他的代碼, 宣布工作完成, 接著他就可以飛到馬爾代夫去度假去了.
可以看到, 盡管我們的功能八字還沒一撇, 可只要我們堅持面向接口編程, 時時想著團隊合作, 經常提交已經寫好的代碼, 特別是公共接口方面的代碼, 我們的同事就能及時推進他們的工作, 甚至比我們還早完成, 這都是有可能的, 都是正常的, 也是我們應該追求的.
而利用好抽象及封裝, 我們還能得到好幾個好處:
可測試性(Testability)
通過以上舉例, 可以看到, 我們可以手動構建一個圖案, 并交給程序去判斷(注: 為了簡短起見, 代碼中省略了具體的 assert
細節). 而如果是開頭那樣直接就打印了呢? 你根本沒法讓程序去判斷, 只能通過人眼去觀察輸出, 這樣就給 自動化的測試(Automatic Test) 帶來了困難.
可重用性(Reusability)
當 getPattern
被抽象出來之后, 可以看到, 不但可以在 printPattern
方法里使用, 也可以在 generatePatternFile
方法里使用. 而如果按開頭那樣呢? 你沒法復用, 你還是不得不重構;又或者你可能只是簡單地把代碼復制一遍了, 再作些改動.
當然, 現在這個程序很小, 全部拷貝一遍好像也很快, 但如果是很大的程序呢? 又或者我們又要拓展到可供 web service 調用, 難道就這樣拷貝下去? 哪一天程序要做些小調整, 難道又要一一去修改嗎?
不要重復(DRY: Don’t Repeat Yourself)
管理重復性一直都是程序開發中的重大關切, 在目前這個小程序里, 這一問題還不是那么迫切, 這個在此就不作詳述, 以后會另寫一些文章來做些介紹.
好了, 說了一大通, 繞了一大圈, 測試也測了, 同事也度假去了, 我們也要趕緊我們的工作. 那么接下來是不是趕緊寫那些實現呢? 不!
我們已經介紹了不少的"ility"結尾的單詞, 接下來還要說到!我有點擔心大家說我"zhuangbility", 有句話說: “Don’t zhuangbility, zhuangbility leads to leipility”(莫裝逼, 裝逼遭雷劈), 沒辦法, 為了闡述這些大道理, 我也只好冒著被 leipility 的危險.
可維護性(Maintainability)
你首先把注釋寫好:
怎么說呢? 現在 IT 工作強度很大, 過勞死是不稀奇的事, 寫著寫著說不定哪天人就掛了. 一個人掛了不要緊, 工作可不能掛!(不是在說笑話, 貌似有些公司或老板表現出來的態度就是這樣的~)
別人要能順利接手你的活, 這是關鍵.
其實沒必要說"掛了"這些不吉利的話, 也可能是有人要生了, 比如你老婆要生了, 你也休產假去了, 你寫到一半, 老板把你的工作轉交給你的同事.
試想, 要是一點注釋都沒有, 你的同事接手起來就很困難, 他要加班加點才能早點弄清你的代碼的意圖. 所以呢, 不要害了你的同事!把代碼的可維護性做好, 大家的健康也才有更好的可維護性!
代碼如下:
/*** 獲取指定行數的圖案, 比如3行時: * ** **** ****** * @param lineCount 指定的行數* @return 圖案的字符串表示, 包括換行符在內*/
public String getPattern(int lineCount) {// TODO Auto-generated method stubreturn null;
}
其實, 良好的命名同樣也是可維護性的關鍵, 比如上面的
getPattern
,lineCount
, 而不是像最前面那個示例中的 i, j, k, z 等亂七八糟的名字.另外, 豐富的抽象層次也是如此, 這點我們后續還會不斷提及.
好了, 注釋也寫完了, 然后呢, 現在該輪到寫那個該死的等差數列了吧? 不!
健壯性(Robustness)
Robustness 又常常音譯成魯棒性.
作者在大學時讀的是自動化專業, 在那些自動控制理論里, 老出現什么魯棒性, 看了讓人犯暈, 不如直接叫健壯性.
我倒是想起了小時候老爸常買給我喝的 Robust(即樂百氏, 與娃哈哈類似的飲品), 味道是不錯, 不過喝完身體挺沒見得健壯到哪去, 也許喝得還不夠多~
我們先要把判斷做好, 輸入負數或者輸入的數字太大了, 你要拒絕它們, 同時在注釋中也作出說明:
/*** 獲取指定行數的圖案, 比如3行時: * ** **** ****** * @param lineCount 指定的行數, 1-20之間* @return 圖案的字符串表示, 包括換行符在內*/
public String getPattern(int lineCount) {if (lineCount < 1) {throw new IllegalArgumentException("行數不能小于1!");}if (lineCount > 20) {throw new IllegalArgumentException("行數不能大于20!");}// TODO return null;
}
我知道我在這里說這些, 有些人可能已經不耐煩了, 他們想著的是寫那些有技巧的代碼, 那些有挑戰性的部分, 那些 tricky 的部分, 那些能體現出他們智商上的優越感的部分.
有個詞是怎么說的, “rocket science”(火箭科學, 喻指那些高精尖的技術), 特別的有些剛畢業的心氣很高的學生, 滿腦子想的可能就是這些. 可是呢, 類似情況不是沒有, 但通常是很少的:
騷年, 不是我在打擊你, 你也許真的想多了. 工作上, 我們多數時候處理的都是一些細節的問題, 一些瑣碎的事情, 一些按部就班的樣板式的代碼, 需要的不是多高的智商, 多么 tricky 的技巧, 要是是耐心, 細致, 嚴謹, 一絲不茍.
為何一開始就要把這些做好呢? 因為到了后面, 你就沒時間去做了. 這一點你一定要相信我, 以下引自 wiki 的"90-90法則":
前 90% 的代碼要花費你 90% 的開發時間, 剩余的 10% 的代碼要花費你另一個 90% 的開發時間.
The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time.
–Tom Cargill, 貝爾實驗室
而最后如果因為時間緊急, 就這樣沒保護就上了生產環境, 一旦出了問題, 你會花更多的時間去收拾這些爛攤子, 而最終你還是不得不將這些補上.
有一個"墨菲定律"(Murphy’s Law)大意是這么說的:
有可能出錯的的東西一定會出錯.
現在不擦屁股, 后面還有得擦. 你省掉了紙尿褲, 你的程序就裸奔了, 你就等著洗更多的外套.
我們也常說: "該來的一定會來. "如果用電影<<無間道>>里的話來說呢, 那就是:
“出來混, 遲早要還的”. (哇塞, 說得太精彩了. 這些編導或者劇作家不去寫教科書太可惜了. )
所以呢, 不要有僥幸的心理, 把程序從一開始就寫健壯才是正道.
小結
說了半天, 我們甚至連一行核心代碼都沒寫, 不過, 文章至此倒是要先做一個階段了結了. 我們說寫代碼有個原則, 那就是方法不能太長, 最好一個屏幕就能顯示完, 否則看起來就很累了;自然的, 文章也不能寫得太長, 否則寫起來, 讀起來都很累人.
所以呢, 雖然一開始那里提了好多的道理, 本來也是想一扒到底的, 但扒到一半發現已經很長了, 所以上半身扒完, 就此腰斬, 下半身留待后面繼續扒, 下半身更精彩, 我們下回再見.
下一篇, 見 小程序中的大道理之二 .