日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯網營銷解決方案
一個非教條式的TDD例子

問題背景

創(chuàng)新互聯建站是專業(yè)的膠州網站建設公司,膠州接單;提供成都網站設計、網站制作,網頁設計,網站設計,建網站,PHP網站建設等專業(yè)做網站服務;采用PHP框架,可快速的進行膠州網站開發(fā)網頁制作和功能擴展;專業(yè)做搜索引擎喜愛的網站,專業(yè)的做網站團隊,希望更多企業(yè)前來合作!

數據分批器這個名字是我臨時起的一個名字,源于我輔導的客戶團隊開發(fā)人員在當時的核心系統中要解決的一個實際業(yè)務問題 —— Oracle的數據庫刪除每次只支持1000條。這個問題更確切的講是因為Oracle對下面這句SQL語句的支持約束:

delete from t_table where id in (ids)

問題就出在這個where id in ...上,后面?zhèn)魅氲募蠀礽ds最大支持1000條。而實際業(yè)務場景中存在大于1000條數據,所以需要進行分批處理。

針對這個問題,我暫時不去探究這個SQL機制本身的合理性[1]。本文我想借這個機會聊聊如何運用TDD的方式去完成這個數據分批處理的設計和開發(fā)。

需求分解

基于這樣的問題,我的習慣是先對問題進行分解,也就是TDD前要做的一個關鍵動作 —— Tasking。我快速做了個Tasking:

(1)非1000的整數倍,小于1000條

(2)1000的整數倍

(3)非1000的整數倍,大于1000條

基于Tasking結果,對上述需求場景進行實例化,實例化過程中,邊界值是我要考慮的重點:

(1)非1000的整數倍,小于1000條

  • 0條
  • 369條

(2)1000的整數倍

  • 1000條
  • 2000條

(3)非1000的整數倍,大于1000條

  • 1369條
  • 2222條

原有設計

上述代碼的for循環(huán)中做了兩件事,邊截取不同范圍的recordsId,邊調用了 CustomCustomerRiskScoreRepository 來做數據庫操作,分批邏輯和存儲夾雜在一起的過程式設計的好處是直接了當,當然也讓 deleteByRecordIds() 的職責過多。

新的設計

基于我對軟件設計淺薄的理解,我認為這個分批邏輯和Repository數據存儲邏輯分開會更優(yōu)雅,支持我的幾個主要理由是:

  • 數據存儲邏輯更加純粹,它只用關心數據的CRUD。
  • 重要:分批邏輯可以很方便地進行自動化測試。
  • 分批邏輯獨立出來,方便復用和維護。

基于以上的幾點理由,我在原來的過程式程序設計的基礎上引入一些OO的理念,進行對象建模,比如抽象出一個數據分批器。我把對象建模過程看做在TDD之前的一些簡單且必要的設計。

TDD不太提倡在開始前不做任何設計,恰恰它提倡做一些簡單且必要的程序接口設計 —— 非教條主義

通過對象建模分析,我設計了兩個簡單的對象,一個是BatchDivider,另一個是Range,UML如下:

BatchDivider接收一個總數,然后能夠返回一個包含了起止的范圍的集合,比如接收1369條,返回集合[Range(0, 1000), (1000, 1369)],起止信息我用Range對象來表示。

前期的設計就做到這,我沒有花太多時間去糾結細節(jié),因為現在的設計足夠讓我起步了。如果在后續(xù)發(fā)現了不完善的地方,交給后續(xù)的驅動和重構。

編碼落地

至此,我已經把需求進行了分解和實例化,然后也做了簡單的程序設計,跑步前的熱身完畢,接下來我就打算采用TDD的跑姿小跑起來。

之前的實例化我寫得比較簡單,由于我習慣用Given-When-Then的方式來描述,我把之前簡單的實例化再結合當前的程序設計做了細化:

非1000的整數倍,小于1000條

(1)0條

  • Given 待分批數據是0條
  • When 分批處理
  • Then 批次結果為空集合, []

(2)369條

  • Given 待分批數據是369條
  • When 分批處理
  • Then 批次結果包含1個Range的集合,[(0, 369)]

1000的整數倍

(1)1000條

  • Given 待分批數據是1000條
  • When 分批處理
  • Then 批次結果包含1個Range的集合,[(0, 1000)]

(2)2000條

  • Given 待分批數據是2000條
  • When 分批處理
  • Then 批次結果包含2個Range的集合,[(0, 1000), (1000, 2000)]

 非1000的整數倍,大于1000條

(1)1369條

  • Given 待分批數據是1369條
  • When 分批處理
  • Then 批次結果包含2個Range的集合,[(0, 1000), (1000, 1369)]

(2)2222條

  • Given 待分批數據是2222條
  • When 分批處理
  • Then 批次結果包含3個Range的集合,[(0, 1000), (1000, 2000), (2000, 2222)]

第1個測試

  • Given 待分批數據是0條
  • When 分批處理
  • Then 批次結果為空集合, []

在編寫測試代碼時,有一種做法是從結果往前驅動,比如我會先寫assert,然后按照思維意圖一步一步往前倒逼我去命名變量,去給變量賦值。下面是我寫的第1個測試用例:

你現在看到的5 ~ 11行代碼,我的編寫順序是11 --> 5。這僅僅是我個人的習慣,因為受到以終為始和意圖導向編程觀念的影響,養(yǎng)成了這樣的習慣。Kent Beck在《測試驅動開發(fā)》也提到了這種方式。

按照反向驅動的方式寫出來的代碼,我發(fā)現測試代碼中的一些沒必要的臨時變量,但我先不著急去重構,我先聚焦讓這個測試通過,至少現在的代碼可讀性也非常好。很快,借助IDE的快捷鍵,我編寫了讓測試通過的實現代碼:

我使用了偽實現讓測試快速通過,緊接著借助Inline手法清除了測試代碼中沒必要的臨時變量totalItemSum和batchCount。

第2個測試

  • Given 待分批數據是369條
  • When 分批處理
  • Then 批次結果包含1個Range的集合,[(0, 369)]

第二個測試起名為 given_item_count_below_1000 ,這里我沒有使用具體的實例化數據369,你或許會起名為 given_item_count_is_369。我個人習慣采用抽象場景來命名,因為我覺得能夠更直觀的提供業(yè)務場景的信息。

第二個測試,我直接給ranges添加了一個Range對象,并保留了上一個測試的判斷,快速地讓測試通過了:

第3個測試

  • Given 待分批數據是1000條
  • When 分批處理
  • Then 批次結果包含1個Range的集合,[(0, 1000)]

這個測試運行之后直接通過了,萬事大吉,運氣還不錯。

第4個測試

  • Given 待分批數據是2000條
  • When 分批處理
  • Then 批次結果包含2個Range的集合,[(0, 1000), (1000, 2000)]

為了通過這個測試,我編寫了如下的功能代碼,節(jié)省篇幅,我只摘取了核心的batchProcess方法:

很順利,我通過了第四個測試,我發(fā)現該方法中1000這個數字出現了好幾次,這是個魔法數字,而且重復出現,在我看來這就是一只攜帶了兩種味道的惡魔,于是抽取一個常量ONE_BATCH_SIZE。

正當我準備寫下一個測試時,我運行了之前的所有測試,發(fā)現第2個測試掛了 —— 我破壞了原來的功能。我回頭檢查了 batchProcess 方法的實現,原來是 totalItemCount / ONE_BATCH_SIZE 這個運算出了問題,當totalItemCount = 369的時候,整除結果為0。

定位到問題,基于之前的開發(fā)經驗,我嘗試了Math函數的幾個方法,最終找到了 Math.ceil(double) 方法:

修復第2個測試之后,我心里給TDD點了個贊:TDD所構建的測試安全網可以為重構提供保護,如果功能被破壞,測試很快就能發(fā)現,這讓我去更有勇氣去重構。

第5個測試

  • Given 待分批數據是1369條
  • When 分批處理
  • Then 批次結果包含2個Range的集合,[(0, 1000), (1000, 1369)]

第6個測試

  • Given 待分批數據是2222條
  • When 分批處理
  • Then 批次結果包含3個Range的集合,[(0, 1000), (1000, 2000), (2000, 2222)]

這個測試也直接通過了,到目前為止,所有的測試運行結果都是綠色的?;诋斍暗?個測試,我對當前的程序非常有信心了,先前實例化的場景也都覆蓋了,所以我準備停下來去倒杯水。

可規(guī)避的教條

教條:提前設計不可有

不是說TDD指Test-Driven Design,設計是由測試驅動出來的嗎,提前設計怎么交待呢?

TDD通常也會被解讀為:測試驅動設計。關于測試驅動設計,我覺得一點點提前設計是有必要的,它給了我一個宏觀的方向,讓我能夠順利地開始。我的個人習慣是,在開始寫測試代碼前我會做一些簡單的紙面設計,做一些簡單的對象建模,定義好一些對外的接口。在早期,我也不會寫紙面的設計,而是直接開始寫測試代碼,實際上我在寫測試的時候腦海里是有設計的,只是我沒有顯性化出來。

一方面由于年齡增加,另一方面,面臨的問題很多時候沒這么簡單,顯性化出來可以減輕我大腦負載,并且還能帶來一些其他的好處。

另外,我發(fā)現Uncle Bob在做TDD的時候也會做一些簡單必要的提前設計。比如,他在演示TDD Kata 保齡球的時候就這么干過。

教條:新增測試要見紅

TDD循環(huán)圈紅-綠-重構,新增的測試必須要掛掉,上面第3、5、6,三個測試都直接綠了,你這TDD是在騙人的吧?

之前公司有Senior的同事就和我恨恨地討論過這點,說實話我沒有什么理由去反駁這個觀點,但我沒想明白的是為什么新增一個測試如果直接通過了就不是TDD了呢。測試直接通過了不是更好嘛?當然前提是你的測試是對的。

后來我觀察了很多初學者在練習TDD時的做法,搞明白這種想法背后的擔心。我看有人寫了第一個測試,然后花很多時間去寫實現,結果一股腦實現了很多場景的功能。講真,這樣做也未嘗不可,但從體會TDD的聚焦性和驅動性上來看,這個做法恐怕難以達到目的,這也是TDD門檻高的原因之一 —— 難以控制自己。但從實用性來看,一股腦寫完好幾個場景的功能實現,然后補上后面幾個場景的測試,也并非不可。要知道咱們寫代碼的初衷是什么 —— 交付可用軟件,或美其名曰交付可用的高質量軟件。

所以,為了在學習TDD時更好自控,就有了這樣的一個教條 —— 新增的測試一定得是掛的。但很多時候在編碼過程中,由于編程語言的便捷性,我們在快速、簡單地實現了某個邊界場景用例時難免會存在讓下一個測試直接通過的情況,比如我在寫第4個測試的時候,我直接使用了這個算法:

int batchCount = (int) Math.ceil(totalItemCount / (double) ONE_BATCH_SIZE);

我沒有再花心思去想怎么搞個hard code參數是2000,再加一個if判斷。因為基于前面幾個測試的知識和經驗積累,我已經掌握了讓當前測試通過的算法,而且不會比我hard code更花時間。要說快,我直接上這個會更快。寫完后我其實能預測到后面兩個測試會直接通過,但我還是會加上后面兩個測試,因為這不僅僅關乎TDD的姿勢,更關乎TDD本真的東西 —— 測試保護網。

另外,也可能是由于在拆分任務的時候太細,使用了不同的數據來實例化了同一類場景,導致測試用例有交叉,又或者是拆分場景不合理產生了重復,此時也是一個反饋調整任務列表的契機。

教條:通過測試唯快不破

TDD循環(huán)圈強調要以最快的速度、最簡單丑陋的方式讓測試快速通過,你這寫多了?。?/p>

這個教條跟上一個是緊密相關的,在上一條后半部分也在解釋這個。有時候我明明寫一個功能完整的代碼比簡單、丑陋的代碼要更快,我就不會再去hard code,然后假裝慶祝自己是在做“真”TDD。Kent Beck在《測試驅動開發(fā)》一書的可運行模式中提到,快速讓測試通過的三種方法:

  • 偽實現
  • 三角法
  • 顯明實現

我理解這種觀點的支撐點在于對偽實現和三角法使用,它認為設計應該是通過大量實例化的測試用例驅動出來的。對于那些很復雜的業(yè)務場景,通過簡單的幾個用例確實沒法有效看清抽象模式,浮現不出良好的設計,偽實現和三角法不失為一種好的驅動方式。但有時候,你對設計很清楚,實現很明顯,這個時候何不直接上呢?我在第2個測試的實現中采用了偽實現,而在第4個中由于我破壞了之前的測試,我直接上了Math.ceil函數,沒想到實現了最終的功能。

我提倡的“教條”

提倡:顯性化知識

這么簡單的業(yè)務需求,我腦袋瓜子完全夠用,有必要這么麻煩顯性地呈現出來嗎?

我認為有必要寫出來,俗話說好記性不如爛筆頭。在整理呈現的過程中,我會投入更多的時間思考,思考有沒有準確理解業(yè)務?思考如何能夠讓別人更容易理解?另外,它還可以作為一個顯性的計劃工具,幫助我評估我未來的工作。有了它,我跟團隊成員溝通起來也更高效,并且在后續(xù)非單線程的工作模式中更容易聚焦重點,更方便我去檢查任務進展。在我看來,可視化的動作能讓Tasking發(fā)揮以下四個工具價值:

  • 顯性的計劃工具
  • 高效的溝通工具
  • 便捷的檢查工具
  • 進化的思維工具

話說回來,寫出來并不會花太多時間吧,寫不出來就去做,有可能是因為還想不清楚,想不清楚的話就有危險了。問題域越復雜,這里越值得花時間去想。

提倡:夯實基本功

總是拿玩具代碼來忽悠新人,在實際項目中遠比這復雜的多,還是搞不定呀?

說到這個,我想起了我跟羽毛球的故事。我是2020年8月份的時候才正式接觸羽毛球,在這之前我平均一年打球不到5次,在這之后我最多的時候一周就超過了5次。以前,我無知地以為羽毛球沒什么復雜的,拿個拍子,場上跑跑,基本上人人都會,壓根用不著花錢找教練。后來場上數次被虐的“恥辱”讓我不得不去好好了解一下羽毛球。不學不知道,一學嚇一跳,簡單羅列一下:

  • 身體基本功:體能、爆發(fā)力、耐力、反應速度、彈跳
  • 基礎:發(fā)力、握拍、舉拍、步伐(6個方向,1~3步)
  • 技巧:發(fā)球、接發(fā)球、吊球、勾對角、平抽、高遠球、殺球、接殺球、封網、搓球
  • 站位:男單、女單、男雙、混雙

上面列出來的還不齊全,并且基本功和技巧都涵蓋了正手和反手。羽毛球的技巧和戰(zhàn)術如此之多,每一個都夠練習一陣子,羽毛球的羽字:一分為二,學習 + 練習,這個字讓我感慨真不是隨意取的(為了幫助廣大球友快速成長,我整理了羽球知識庫,請在文末獲取[2])。

我可以在這些技巧都不會或者都不嫻熟的情況下,跳過針對性的基本功練習,直接上場叫囂要跟高手拉練,難免自取其辱還得不到有效提升。認識到這點之后,唯有日練揮拍近千次來夯實基本功方可讓我有勇氣嘗試挑戰(zhàn)高手。

“練球不練功,打球一場空”。我認為學習TDD也是一樣的道理,實際項目中確實業(yè)務和架構復雜得多,但這不該成為你拒絕以簡單的案例入手去練習基本功的理由。再復雜的案例,元能力是類似的。

早在2018年底的,我給武漢某Offshore團隊做了一場OOBootcamp,大家覺得ParkingLot不過癮,在訓練營快結束的時候,我?guī)Т蠹姨接懥嗽谝粋€Java SpringBoot后端分層架構中如何落地TDD。有了訓練營的刻意練習,團隊很快在后端分層架構中實踐落地了TDD。

所以,我個人認為基本功是首要的,如果你還沒有嘗試過TDD,不妨先拋開懷疑,先去嘗試一下。

寫在最后

我接觸TDD有7年多時間了,至今未通透,仍在努力學習中。在實際項目落地TDD以及在TDD訓練營中,我會堅持一些原則,主要有以下三點:

  • Tasking優(yōu)先性
  • 設計輕量性
  • 知識明顯性

對業(yè)務需求進行Tasking的過程能夠讓我重新梳理業(yè)務,并且減少理解上的偏差和遺漏,避免在后續(xù)開發(fā)過程出現返工,造成更高的修改成本。即便你做不到測試先行,Tasking也值得去做好。

設計本身就是一種不可言說的知識,何為簡單且必要的設計?何為恰到好處的設計?不是看了幾本書、借用幾個大牛的三兩句話就能說清的,更多源自于日常實踐中的思考和總結。

針對Tasking的產出物,我會使用可視化和結構化的方式顯性化管理起來,以便跟業(yè)務人員溝通確認,并更好地在團隊內共享。

三條中前兩條對個人底層勝任力提出了要求。Tasking做得質量如何,比較依賴個人的分析性思維,設計做的好不好,考驗的是一個人對設計的Sense。這些能力不會那么輕易通過短期的培訓得到提升,但好在我們每個人都具備一些基礎。在這樣的基礎前提下,結合一些結構化良好的顯性知識管理方式,堅持跟時間賽跑吧。

補充說明

改進

在這個數據分批器案例中,測試用例和設計上都有可以改進的地方,比如:

  • 測試用例使用jUnit 5的參數化測試,測試斷言的優(yōu)化;
  • 消除「域冗余」:方法BatchDivider.batchProcess() --> BatchDivider.process();
  • BatchDivider也可以做得更簡單,讓其成為一個靜態(tài)工廠方法,變身為工具類;
  • BatchDivider還可以將分批處理的結果存起來,方便重復獲取;
  • BatchDivider再或者后期需要支持動態(tài)分批,比如500一批,2000一批;
  • 喜歡Java Stream API的小伙伴可以Stream來實現BatchDivider
  • 等等其他未知改進;

改進無止境,希望我想分享的點得到傳達,更多的優(yōu)化留給文后有心的你。

手稿

過程我大概花了45分鐘的時間,從需求分解、前期設計、代碼庫的搭建和功能實現,為了節(jié)省時間,我原先用的是手稿,文中是我后來另做了文字化。下圖是留作紀念的手稿:

? ?

原文鏈接:??一個非教條式的TDD例子 (qq.com)??


分享名稱:一個非教條式的TDD例子
網站鏈接:http://www.5511xx.com/article/cdojgho.html