小步
小步進(jìn)行(Baby steps)是TDD開發(fā)中非常重要的理念。
- 小步增加測(cè)試暖夭,確保了不會(huì)一次引入過多變更箩祥。以免一不小心扯了蛋。:D
- 小步通過測(cè)試驯鳖,確保了最快最直接的從紅燈轉(zhuǎn)為綠燈闲询。避免前期過度設(shè)計(jì),以及隨之而來的無測(cè)試的實(shí)現(xiàn)代碼浅辙。
- 小步重構(gòu)扭弧, 確保了安全平滑的改善代碼結(jié)構(gòu)。
在整個(gè)TDD“紅燈->綠燈->重構(gòu)”的循環(huán)中记舆,小步提供了快速反饋?zhàn)屛覀冸S時(shí)檢驗(yàn)代碼行為鸽捻,并在出錯(cuò)的第一時(shí)間進(jìn)行修正。另一方面也促使我們“一次只做一件事”,將一個(gè)較為復(fù)雜的問題分解開來逐步解決御蒲。
僵局
看起來很簡(jiǎn)單是吧衣赶,但是現(xiàn)實(shí)往往是骨感的。在學(xué)習(xí)TDD的過程中厚满,我們經(jīng)常會(huì)發(fā)現(xiàn)小步成為了一大難點(diǎn)府瞄。一開始的紅燈然后hardcode綠燈還算簡(jiǎn)單。之后碘箍,要么反復(fù)的小步踏步摘能,要么對(duì)這種小步探索喪失信心,重新回到了 長(zhǎng)時(shí)間預(yù)先設(shè)計(jì) -> 大改動(dòng) -> 調(diào)試 的節(jié)奏中敲街。
對(duì)這個(gè)問題团搞,Unclue Bob提出了一個(gè)指導(dǎo)意見, Transformation Priority Premise (代碼變換優(yōu)先次序)簡(jiǎn)稱 TPP多艇。由于整個(gè)的優(yōu)先列表比較復(fù)雜逻恐,而且還有些可商榷的地方,今天在這里并不打算詳細(xì)介紹峻黍。有興趣的同學(xué)可以看看文末鏈接复隆。這里只對(duì)比最近一次coding dojo的例子,分享一下這個(gè)原則對(duì)小步練習(xí)的啟發(fā)姆涩。
蠢代碼 聰明代碼
在第一個(gè)“紅燈->綠燈”循環(huán)后挽拂,有一個(gè)問題就擺在了程序員面前:既然要求最快的通過測(cè)試,那么為什么不一直hardcode來通過測(cè)試呢骨饿?
以我們所做的“羅馬數(shù)字轉(zhuǎn)換為整數(shù)”的Kata為例亏栈,在一開始hardcode
if(roman.equals("X")) {
return 10;
}
if(roman.equals("V")) {
return 5;
}
if(roman.equals("I")) {
return 1;
}
到
...
if(roman.equals("II")) {
return 2;
}
之后宏赘,為什么不寫3千句if來解決這個(gè)題目呢绒北?
其實(shí)我們心里都明白,一句永遠(yuǎn)返回一個(gè)值的代碼太蠢了察署。它并不真正“理解”并解決問題闷游,我們的目標(biāo)是要寫一段“聰明”的代碼來真的解決它。難點(diǎn)在于贴汪,我們習(xí)慣于一開始就尋找“聰明”的方案脐往,從問題的“本質(zhì)”去解決。一旦刻意縮小步伐扳埂,先寫下一句蠢代碼业簿,卻發(fā)現(xiàn)找不到一條路把它逐步變的聰明起來。
constant -> constant+
這里就引入了TPP中的一步聂喇,constant -> constant+
當(dāng)我們開始寫II轉(zhuǎn)換為2的邏輯時(shí)辖源,首先增加了一個(gè)硬編碼的分支來通過測(cè)試
這時(shí)蔚携,可能我們心里會(huì)覺得有一些不對(duì),感覺并沒有真正在解決這個(gè)問題克饶。那么酝蜒,如何開始讓代碼向”真正“的代碼演進(jìn)呢?
下面就是采用了constant -> constant +規(guī)則來重構(gòu)后的代碼
if(roman.equals("I"+"I")) {
return 1+1;
}
哇哦……
好吧矾湃,其實(shí)好像沒什么大不了的嘛亡脑。從編譯運(yùn)行來說,"I"+"I"和1+1很可能會(huì)被編譯器直接優(yōu)化成為常量"II" 和2邀跃,也就是說這么改寫對(duì)機(jī)器來說霉咨,根本就是一樣的嘛。
但是代碼是給人看的拍屑。
這個(gè)變換的最大意義途戒,在于減少了代碼中的知識(shí)。
知識(shí)
知識(shí)僵驰,在日常生活中是個(gè)很好的詞喷斋。“知識(shí)就是力量”蒜茴,“知識(shí)改變生活”星爪,見多識(shí)廣是美德。
然而粉私,軟件中的知識(shí)卻并非如此顽腾。對(duì)于代碼而言,知識(shí)意味著無法用邏輯解釋的事情诺核。比如為什么羅馬人用I表示1抄肖,而不是用Y。這是代碼無法回答的問題猪瞬。一個(gè)知識(shí)對(duì)于代碼而言就是一個(gè)依賴或者假設(shè)憎瘸。怎么處理這些依賴和假設(shè)是軟件設(shè)計(jì)的關(guān)鍵問題。
在代碼世界中“你知道的太多了”可能是更合適的格言
以非常常見的兩個(gè)設(shè)計(jì)原則為例:
- 消除重復(fù) (DRY):這條規(guī)則往往被理解為消除重復(fù)的代碼陈瘦。著眼點(diǎn)在于避免重復(fù)勞動(dòng)。比如上面例子中“II”改寫為"I" + "I"后潮售,可能很快就會(huì)有人意識(shí)到"I"可以提取替換來消除字面量的重復(fù)痊项。然而比這個(gè)更重要的是,消除知識(shí)的重復(fù)酥诽。也就是說對(duì)于每個(gè)知識(shí)鞍泉,應(yīng)該有且只有一個(gè)地方對(duì)其進(jìn)行表述。
- 單一職責(zé):這里的職責(zé)肮帐,往往也被局限在代碼功能方面咖驮。對(duì)于不怎么需要編碼的事情边器,往往就忘記它也可能和職責(zé)相關(guān)了。比如當(dāng)前時(shí)間托修,比如pi=3.14忘巧。事實(shí)上,職責(zé)最重要的一點(diǎn)就是知識(shí)的所有權(quán)睦刃。對(duì)于每項(xiàng)知識(shí)砚嘴,應(yīng)該有一個(gè)所有者對(duì)它“說了算”,系統(tǒng)的其它部分都聽它的涩拙,這樣才能保證所有相關(guān)于這個(gè)知識(shí)的改變都局部化在這個(gè)單元之中际长。
我們?cè)倩仡^以知識(shí)的視角來看2->1+1的這一次變換。前面的I->1,V->5,X->10……是羅馬數(shù)字本身所固有的知識(shí)兴泥。軟件中必須要寫下處理這些知識(shí)的代碼工育,當(dāng)然,可以寫的比一堆if優(yōu)雅很多搓彻,不過這是另一個(gè)話題了如绸。
然而 II->2 卻不同。它是 I->1以及數(shù)字組合邏輯的派生結(jié)果好唯。簡(jiǎn)單的增加一句if是向系統(tǒng)中增加了一個(gè)知識(shí)竭沫,而這個(gè)知識(shí)本身是可以消除掉的。
通過把本來看似一整塊的常量寫成常量的組合骑篙,讓本來隱含的知識(shí)重復(fù)變成了明顯的代碼重復(fù)蜕提。為后續(xù)的提取常量/變量等后續(xù)重構(gòu)開辟了道路。
參考資料
TPP: https://blog.8thlight.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html
漫畫版: http://blog.8thlight.com/uncle-bob/2013/05/27/TransformationPriorityAndSorting.html