Code Kata 之讀自己過(guò)去的代碼

臨近過(guò)年的日子,是個(gè)回顧的好機(jī)會(huì)。
正好前一段看到這篇翻譯介紹 讀自己以前代碼的Kata食棕,拿來(lái)練習(xí)練習(xí)簿晓。
其實(shí)Thomas在提出Kata概念的時(shí)候千埃,涵蓋的范圍是遠(yuǎn)大于編碼層面的放可。只是由于其它類型的Kata的判定標(biāo)準(zhǔn)比較“虛”耀里,所以不常拿來(lái)練習(xí)。
這個(gè)Kata的關(guān)注點(diǎn)在于刻意練習(xí)閱讀代碼底哥,而且是自己寫的代碼房官,來(lái)體會(huì)代碼中各種細(xì)節(jié)的長(zhǎng)期影響翰守。通過(guò)主動(dòng)的回顧來(lái)形成良好習(xí)慣,并增強(qiáng)review能力拒课。
我自詡是個(gè)“代碼考古學(xué)家”早像,常常修一個(gè)bug把80%的時(shí)間用在了翻歷史記錄,來(lái)琢磨它為啥能寫成這個(gè)樣子臀脏。同時(shí)也是個(gè)熱衷于review別人代碼的人揉稚,不憚?dòng)趯?duì)別人的代碼說(shuō)三道四熬粗,想必很惹人厭了驻呐。
這次算是找個(gè)機(jī)會(huì)把自己的代碼擺出來(lái)灌诅,自食其果吧。

程序員讀到自己的代碼

代碼的選擇

Kata的要求是寫了超過(guò)一年含末,記憶有些模糊猜拾。代碼量在500至1000行。
我選擇了一個(gè)小項(xiàng)目佣盒,小到基本由我一個(gè)人開(kāi)發(fā)挎袜。而且除了對(duì)口的產(chǎn)品經(jīng)理,也再?zèng)]其他人關(guān)心了肥惭。所以當(dāng)初可以隨心所欲的搞盯仪。
然而即便這么小的項(xiàng)目务豺,做完以后客戶卻要求與最初商定的需求文檔幾乎完全不一樣的功能磨总。最終,所有的業(yè)務(wù)對(duì)象推倒重新做了一遍笼沥。所以也可以算是個(gè)典型的軟件項(xiàng)目吧蚪燕。
基本上是用outside-in 式TDD開(kāi)發(fā),雖然當(dāng)時(shí)還不是特別自覺(jué)的采用這種流程奔浅。

從中選擇了一個(gè)類來(lái)做Review馆纳。選擇時(shí)參考了《修改代碼的藝術(shù)》作者關(guān)于技術(shù)債分析的建議:一個(gè)類修改越頻繁,說(shuō)明它越可能將來(lái)被改動(dòng)汹桦,越可能存在技術(shù)債鲁驶。
我統(tǒng)計(jì)了所有的提交記錄,選出一個(gè)提交次數(shù)最多的類舞骆,名叫WorkflowManager钥弯。和標(biāo)準(zhǔn)工作流沒(méi)什么關(guān)系径荔,是一個(gè)實(shí)現(xiàn)簡(jiǎn)單工作步驟流轉(zhuǎn)的類。

第一遍讀:假定這是大牛寫的代碼脆霎,記錄驚艷的地方

  • 短总处。類只有200行,每個(gè)方法的長(zhǎng)度都少于30行睛蛛。
    雖說(shuō)是個(gè)簡(jiǎn)單的流程控制鹦马,用語(yǔ)言來(lái)描述還真不是一兩句話能說(shuō)清的。相比起來(lái)代碼算是相當(dāng)簡(jiǎn)潔了忆肾。
  • 一致荸频。代碼格式,命名規(guī)范客冈。常用寫法都很一致旭从。看起來(lái)很整齊场仲。
  • 可讀遇绞。方法和變量都起了有意義的名字。
  • 測(cè)試覆蓋燎窘。類共有15個(gè)方法,對(duì)應(yīng)的測(cè)試有19個(gè)用例蹄咖。測(cè)試代碼量是實(shí)現(xiàn)代碼的兩倍褐健。沒(méi)有專門用工具統(tǒng)計(jì)覆蓋率,按照從其它項(xiàng)目得到的經(jīng)驗(yàn)澜汤,應(yīng)該在90%以上蚜迅。
  • 每個(gè)測(cè)試都使用了統(tǒng)一的Given/When/Then格式。
  • 每個(gè)測(cè)試驗(yàn)證一個(gè)概念俊抵。對(duì)于相同方法不同使用情境的情況谁不,分別寫對(duì)應(yīng)的測(cè)試用例。
  • 在測(cè)試中使用有意義的變量命名和assert message來(lái)說(shuō)明測(cè)試的意圖徽诲,比如:
    //Given
    User original = new User("assignee before operation");
    workSet.assignee = original;
    //When
    manager.operate(workSet);
    //Then
    assertEquals("Not change assignee when operate work set", original, workSet.assignee);

第二遍讀:假定這是個(gè)二貨寫的代碼刹帕,記錄爛的地方

其實(shí)在看第一遍的時(shí)候就開(kāi)始忍不住想這些地方應(yīng)該寫的更好了。我果然是不善于發(fā)現(xiàn)優(yōu)點(diǎn)谎替,專愛(ài)挑毛病偷溺。

  • 單一職責(zé)。
    • 這個(gè)類叫做WorkflowManager, xxxManager這個(gè)名字暗示了職責(zé)不清钱贯。
    • 果然挫掏,通過(guò)Clean Code中學(xué)到的辦法,統(tǒng)計(jì)每個(gè)方法與成員變量間的關(guān)系秩命。發(fā)現(xiàn)每個(gè)成員變量都只被很少幾個(gè)方法使用尉共。說(shuō)明這個(gè)類缺乏內(nèi)聚性褒傅,是多個(gè)職責(zé)的雜燴。
    • 在測(cè)試?yán)镆灿畜w現(xiàn)袄友,某幾個(gè)用例關(guān)注于某個(gè)方法殿托。它們依賴共有的前置條件,驗(yàn)證相似的結(jié)果杠河÷刀看起來(lái)就像是Test類里內(nèi)嵌了一個(gè)小Test類。
  • 未使用的參數(shù)券敌。說(shuō)明當(dāng)初是覺(jué)得“將來(lái)會(huì)用到”加上的這個(gè)參數(shù)唾戚,而不是由測(cè)試驅(qū)動(dòng)產(chǎn)生的。
  • 注釋待诅。實(shí)現(xiàn)和測(cè)試代碼中都有說(shuō)明意圖的注釋叹坦。應(yīng)該重構(gòu)代碼使注釋沒(méi)有必要出現(xiàn)。
  • 系統(tǒng)時(shí)間卑雁。實(shí)現(xiàn)代碼里有最近更新日期的時(shí)間戳字段募书。測(cè)試中在操作完畢后,直接用當(dāng)前系統(tǒng)日期與之比較测蹲。這樣做有兩個(gè)問(wèn)題莹捡。一是依賴環(huán)境,另一個(gè)問(wèn)題是如果測(cè)試執(zhí)行和驗(yàn)證的兩個(gè)點(diǎn)恰好跨0點(diǎn)就會(huì)失敗扣甲。
  • 返回null篮赢。由于依賴的對(duì)象有可能返回null, 這段代碼里相當(dāng)一部分在處理這種情況琉挖,使得主線邏輯有些模糊启泣。可悲的是示辈,之后它又會(huì)返回null值給外面……
  • 命名寥茫。有些方法名用的是類似事件的名稱,比如stateChanged矾麻,而不是一個(gè)動(dòng)作纱耻。
  • 異常處理。代碼里有一部分邏輯是處理配置信息異常情況的射富,這種情況下程序無(wú)法正常完成操作膝迎。然而處理的不夠統(tǒng)一,有些地方悄悄的返回null值胰耗,有些記錄了日志限次,但是沒(méi)有明確指出這是某項(xiàng)配置的問(wèn)題,以及這個(gè)問(wèn)題可能造成的后果。
  • 測(cè)試中assert message很難說(shuō)它是描述了應(yīng)該的行為卖漫,還是失敗時(shí)候的錯(cuò)誤费尽。比如:
    assertNull("no start date after approval", workSet.startDate);
    到底是應(yīng)該有這個(gè)日期還是沒(méi)有呢,
  • 有些message在程序行為變化后沒(méi)有隨之修改羊始。
  • 每個(gè)test幾乎都有重復(fù)的mock權(quán)限相關(guān)數(shù)據(jù)的代碼旱幼,模糊了當(dāng)前測(cè)試的焦點(diǎn)

上面這些嚴(yán)格來(lái)說(shuō),應(yīng)該算是可以改進(jìn)的地方突委,還稱不上是二柏卤。不過(guò)下面這段就……

public List<Item> setSelectedItems(List<Item> items, WorkSet workSet) {
    List<Item> remainItems = new ArrayList<Item>(items);
    nextInputItem:
    for (Item inputItem : items) {
        for (Item item : workSet.items) {
            if (item.key.equals(inputItem.key)) {
                item.selected = inputItem.selected;
                remainItems.remove(inputItem);
                continue nextInputItem;
            }
        }
        if (!inputItem.selected) {
            remainItems.remove(inputItem);
        }
    }
    return remainItems;
}

兩層for嵌套,不但嵌套for匀油,里面還套if缘缚,不但if,還continue敌蚜,不但continue桥滨,還從內(nèi)層continue到外層……
感覺(jué)差不多把今天我認(rèn)為不好的風(fēng)格全拿出來(lái)演練了一遍。瞬間有點(diǎn)“這是我寫出來(lái)的么弛车?”的感覺(jué)齐媒。不過(guò)稍稍回憶一下,就想起來(lái)我當(dāng)時(shí)還去專門查了查continue到外層的寫法纷跛。

你能看出來(lái)這是在干嘛么喻括?
其實(shí)是數(shù)據(jù)庫(kù)里的workset保存有若干個(gè)項(xiàng)目,每個(gè)項(xiàng)目有選中狀態(tài)贫奠。每次頁(yè)面提交時(shí)候會(huì)返回這些項(xiàng)目通過(guò)頁(yè)面操作后的選中狀態(tài)双妨。需要更新到數(shù)據(jù)庫(kù)中。
而且頁(yè)面還有可能增加原來(lái)數(shù)據(jù)庫(kù)里沒(méi)有項(xiàng)目叮阅,這些項(xiàng)目要在數(shù)據(jù)庫(kù)新建項(xiàng)目記錄,并且和workset關(guān)聯(lián)起來(lái)泣特。
這個(gè)雙層循環(huán)做了兩件事

  1. 對(duì)于已經(jīng)有的項(xiàng)目浩姥,按照頁(yè)面?zhèn)魅氲牧斜砀逻x中狀態(tài)
  2. 對(duì)于沒(méi)有的項(xiàng)目,放在另一個(gè)列表中作為返回值状您,方便給下一步做創(chuàng)建項(xiàng)目等操作勒叠。

如果讓你來(lái)重構(gòu)它,你會(huì)怎么做呢膏孟?

第三遍讀:假定這段代碼有個(gè)嚴(yán)重Bug眯分,今天找不出來(lái)你就完了。記錄找到的Bug

雖然沒(méi)有專人QC柒桑,也完全沒(méi)有測(cè)試階段弊决,但是因?yàn)橛芍倚宰訉懥艘欢褑卧獪y(cè)試和集成測(cè)試。每次本地保存代碼的時(shí)候都會(huì)執(zhí)行一遍。因而我對(duì)項(xiàng)目的質(zhì)量是相當(dāng)自信的飘诗。
最直觀的感受是在客戶驗(yàn)收演示會(huì)上与倡。
當(dāng)初開(kāi)發(fā)完成后扔在一邊,拖了半年多客戶才驗(yàn)收昆稿。已經(jīng)記不清具體代碼細(xì)節(jié)了纺座。客戶在操作演示過(guò)程中溉潭,走到一步走不下去了净响。當(dāng)時(shí)我心情毫無(wú)波動(dòng),一點(diǎn)也沒(méi)打開(kāi)源代碼檢查的沖動(dòng)喳瓣。
果然馋贤,稍稍回憶了一下就發(fā)現(xiàn)不是bug,而是操作失誤夫椭。

所以一開(kāi)始我認(rèn)為是沒(méi)法完成這一輪的目標(biāo)的掸掸。準(zhǔn)備能找到個(gè)錯(cuò)誤處理或者很特殊情況下的缺陷就交差吧。
而且讀代碼找bug真的好難蹭秋,要完完全全讀懂每一個(gè)細(xì)節(jié)在做什么扰付。這時(shí)候真巴不得當(dāng)初代碼寫的好讀一點(diǎn)就好了。
沒(méi)想到仁讨,真找到了一個(gè)

public void statedChanged(WorkSetState originalState, WorkSet workSet) {
    WorkPackage workPackage = workSet.workPackage;
    if (isApproved(workSet)) {
        /* ... */
    } else {
        for (WorkSet sibling : workPackage.workSets) {
            if (isPending(sibling)) break;
            workPackage.status = WorkPackageStatus.INPROGRESS;
        }
    }
    workPackageRepository.save(workPackage);
}

這段代碼的本意羽莺,是一次審批計(jì)劃(WorkPackage)中會(huì)分成多個(gè)分組(WorkSet),有任何一個(gè)分組還未有開(kāi)始審批洞豁,那么整個(gè)計(jì)劃都處于未開(kāi)始狀態(tài)盐固。
然而真正實(shí)現(xiàn)的是,只要第一個(gè)分組是進(jìn)行中丈挟,整個(gè)計(jì)劃就會(huì)變?yōu)檫M(jìn)行中刁卜。如果第一個(gè)未開(kāi)始,則整個(gè)計(jì)劃沒(méi)有開(kāi)始曙咽。

相當(dāng)出乎意料竟然有這么明顯的一個(gè)錯(cuò)蛔趴。我反思了一下在重重測(cè)試包圍之下還漏掉這個(gè)Bug的原因。

  1. 有集成測(cè)試例朱,但是沒(méi)有驗(yàn)收測(cè)試孝情。沒(méi)有一個(gè)地方把業(yè)務(wù)流程的故事集中講一遍。因而在較大范圍的測(cè)試中漏掉了這個(gè)點(diǎn)。
  2. 單元測(cè)試中,僅僅構(gòu)造了一個(gè)計(jì)劃有兩個(gè)分組的情況蜀细,沒(méi)覆蓋到所有分支绍弟。
    往深一層想,這個(gè)方法需要判定的情況比較多。所以針對(duì)它的測(cè)試用例本來(lái)就很多串慰,顯得在這個(gè)測(cè)試類中占了很大的比例隧熙。也就是在第二遍讀時(shí)發(fā)現(xiàn)的好像是內(nèi)嵌了一個(gè)小的Test類的情況婉弹。寫的時(shí)候心里就有種喧賓奪主的感覺(jué)睬魂,不由得懷疑是不是對(duì)這個(gè)方法過(guò)度測(cè)試了。進(jìn)而沒(méi)有仔細(xì)考慮來(lái)設(shè)計(jì)用例镀赌。
    其實(shí)內(nèi)心產(chǎn)生不協(xié)調(diào)的真正的原因是實(shí)現(xiàn)類違反了單一職責(zé)氯哮,而不是測(cè)試重心失衡。

感受

這次練習(xí)的收獲收獲頗大商佛,雖然理論上自己有著對(duì)代碼質(zhì)量喉钢,可維護(hù)性,以及測(cè)試用例設(shè)計(jì)等方面的各種觀點(diǎn)良姆。但是這次時(shí)間膠囊式的體驗(yàn)肠虽,還是帶來(lái)了完全不同的心理感受和思索。
特別是第三輪找Bug的挑戰(zhàn)玛追。最后找到的時(shí)候是我記憶中找到Bug最開(kāi)心的一次税课。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市痊剖,隨后出現(xiàn)的幾起案子韩玩,更是在濱河造成了極大的恐慌,老刑警劉巖陆馁,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件找颓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡叮贩,警方通過(guò)查閱死者的電腦和手機(jī)击狮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)益老,“玉大人彪蓬,你說(shuō)我怎么就攤上這事∞嗝龋” “怎么了寞焙?”我有些...
    開(kāi)封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)互婿。 經(jīng)常有香客問(wèn)我,道長(zhǎng)辽狈,這世上最難降的妖魔是什么慈参? 我笑而不...
    開(kāi)封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮刮萌,結(jié)果婚禮上驮配,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好壮锻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布琐旁。 她就那樣靜靜地躺著,像睡著了一般猜绣。 火紅的嫁衣襯著肌膚如雪灰殴。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天掰邢,我揣著相機(jī)與錄音牺陶,去河邊找鬼。 笑死辣之,一個(gè)胖子當(dāng)著我的面吹牛掰伸,可吹牛的內(nèi)容都是我干的怀估。 我是一名探鬼主播狮鸭,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼多搀!你這毒婦竟也來(lái)了歧蕉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤酗昼,失蹤者是張志新(化名)和其女友劉穎廊谓,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體麻削,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒸痹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呛哟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叠荠。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖扫责,靈堂內(nèi)的尸體忽然破棺而出榛鼎,到底是詐尸還是另有隱情,我是刑警寧澤鳖孤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布者娱,位于F島的核電站,受9級(jí)特大地震影響苏揣,放射性物質(zhì)發(fā)生泄漏黄鳍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一平匈、第九天 我趴在偏房一處隱蔽的房頂上張望框沟。 院中可真熱鬧藏古,春花似錦、人聲如沸忍燥。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)梅垄。三九已至厂捞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哎甲,已是汗流浹背蔫敲。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炭玫,地道東北人奈嘿。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吞加,于是被迫代替她去往敵國(guó)和親裙犹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1.測(cè)試與軟件模型 軟件開(kāi)發(fā)生命周期模型指的是軟件開(kāi)發(fā)全過(guò)程衔憨、活動(dòng)和任務(wù)的結(jié)構(gòu)性框架叶圃。軟件項(xiàng)目的開(kāi)發(fā)包括:需求、設(shè)...
    宇文臭臭閱讀 6,724評(píng)論 5 100
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,114評(píng)論 25 707
  • 1.問(wèn):你在測(cè)試中發(fā)現(xiàn)了一個(gè) bug 掺冠,但是開(kāi)發(fā)經(jīng)理認(rèn)為這不是一個(gè) bug 德崭,你應(yīng)該怎樣解決揖盘。 首先,將問(wèn)題提...
    qianyewhy閱讀 9,257評(píng)論 4 123
  • 文章來(lái)自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鵬閱讀 9,192評(píng)論 2 126
  • 新南方新農(nóng)泰培訓(xùn)基地11月1日訊:2017年11月的第一天,參加培訓(xùn)的瑞普集團(tuán)精英們箕慧,經(jīng)過(guò)一天多緊張的理論學(xué)習(xí),于...
    朱道路閱讀 431評(píng)論 0 0