轉(zhuǎn)帖|其它|編輯:郝浩|2010-11-05 11:56:42.000|閱讀 491 次
概述:本篇之所以選擇TDD作為例子,主要是由兩個(gè)原因:1. TDD確實(shí)呈現(xiàn)了設(shè)計(jì)的思路;2. 相對(duì)于DDD來說, TDD更加容易上手,學(xué)習(xí)的曲線沒有那么陡峭 再次申明一下:本系列不是講述TDD的,只是用TDD來建立設(shè)計(jì)的思想。即便是用DDD,有時(shí)候還是結(jié)合TDD一起使用的。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
前言:本篇之所以選擇TDD作為例子,主要是由兩個(gè)原因:1. TDD確實(shí)呈現(xiàn)了設(shè)計(jì)的思路;2. 相對(duì)于DDD來說, TDD更加容易上手,學(xué)習(xí)的曲線沒有那么陡峭 再次申明一下:本系列不是講述TDD的,只是用TDD來建立設(shè)計(jì)的思想。即便是用DDD,有時(shí)候還是結(jié)合TDD一起使用的。
本篇的議題如下:
開發(fā)方式比較
什么是設(shè)計(jì)
設(shè)計(jì)初探
開發(fā)方式比較
我們用下面的一段分析來引出今天的內(nèi)容:
想想我們平時(shí)是如何在寫代碼:
拿來需求,分析功能,編寫功能代碼。
這樣的方式,沒有問題,大家也一直沿用很多年了。為了后面描述方便,我們稱這種方式為傳統(tǒng)流程。
TDD的怎么做的:
拿來需求,分析功能,寫功能測(cè)試代碼,編寫功能代碼。
其實(shí)兩個(gè)過程差不多的,真的差不多的。
首先來分析下兩種開發(fā)流程。個(gè)人認(rèn)為:因?yàn)門DD多了一個(gè)角色轉(zhuǎn)換的過程:在我們傳統(tǒng)流程中,我們一直以一個(gè)開發(fā)人員的思維在想問題,分析,然后就開始實(shí)現(xiàn)。
在TDD中,在分析功能之后,我們就要站在客戶的角度(當(dāng)然很多時(shí)候還是我們自己在模擬客戶)就要檢測(cè)這個(gè)功能是不是真正需要的,然后在這個(gè)前提下,再開始編碼。
下面我們?cè)賮砜匆唤M分析圖:
因?yàn)閺哪玫叫枨蠛屠斫庑枨螅阶詈蟮膶?shí)現(xiàn),這個(gè)過程肯定是有偏差的。就如上圖。
在TDD中,在功能測(cè)試那一個(gè)環(huán)節(jié),就把這種偏差控制了起來。即使最后有偏差,但是小了一些。
為什么要將兩種開發(fā)的方式比較?
首先,從總體上來看,傳統(tǒng)的流程就是先做出基本有用的東西,而且TDD先是搭個(gè)架子,然后在做東西。
在TDD中,我們是直奔功能:針對(duì)需求出測(cè)試,然后針對(duì)測(cè)試出功能。一針見血。可能這些功能暫時(shí)還不能完全用,因?yàn)槿鄙贃|西,如數(shù)據(jù)庫,在測(cè)試中我們可能是模擬的。例如,在實(shí)現(xiàn)一個(gè)功能的時(shí)候,如果這個(gè)功能需要操作數(shù)據(jù)庫或者要通過網(wǎng)絡(luò)訪問,那么我們?cè)谟脗鹘y(tǒng)的方法寫的時(shí)候,想要看看功能最后實(shí)現(xiàn)的效果,往往是debug,或者做出可視化的東西出來,注意力很快就被分散了,如果發(fā)現(xiàn)需求理解不對(duì),之前的就重新來過,代價(jià)可能而知。而采用TDD的方法,可以先寫測(cè)試模擬,如用mock, stub等,這樣關(guān)注點(diǎn)主要在業(yè)務(wù)上,這種方式就好比水波效應(yīng):從中心向周圍擴(kuò)散。
什么是設(shè)計(jì)
一個(gè)軟件系統(tǒng),最重要的就是核心業(yè)務(wù)功能,系統(tǒng)設(shè)計(jì)的時(shí)候,肯定先是分析功能,并且確認(rèn)分析的功能是符合需求的,然后再為實(shí)現(xiàn)功能尋找解決方案。在有了解決方案的前提下,再考慮上技術(shù)的選擇,復(fù)雜性,可擴(kuò)展行,可維護(hù)性,可行性等,最后就”設(shè)計(jì)”就產(chǎn)生了,確定實(shí)現(xiàn)方案之后,最后實(shí)現(xiàn)。”設(shè)計(jì)”確確實(shí)實(shí)是一個(gè)腦力活。
那么我們就來看看,如何做出一個(gè)比較好的設(shè)計(jì)。
做設(shè)計(jì),考慮的太多,太少都不行。多則可能“過度”,少則可能不全。
我們下面就用TDD來幫助我們建立一些設(shè)計(jì)的思想。
在此之前,有一點(diǎn)我想提出:TDD不是測(cè)試,而是設(shè)計(jì)。如果之前一直以為TDD就是寫測(cè)試,那么就說明對(duì)TDD的理解還在“形”上。
設(shè)計(jì)初探
我們之前說過:TDD不是測(cè)試,更多的是設(shè)計(jì)的思路。那么為什么在寫代碼之前寫測(cè)試可以有個(gè)比較好的設(shè)計(jì)?我們就來體驗(yàn)一下。
我們知道,在面向?qū)ο蟮脑O(shè)計(jì)中,有很多的設(shè)計(jì)原則,例如S.O.L.I.D,在系統(tǒng)中充分的使用這些原則,會(huì)導(dǎo)致一個(gè)良性的開發(fā)過程。所以一個(gè)比較的好的設(shè)計(jì),應(yīng)該是盡量的向這些設(shè)計(jì)原則上面靠攏的。
看一個(gè)例子:
例如在用戶訂單管理系統(tǒng)中有一個(gè)需求:客戶在下訂單的時(shí)候首先要去看看自己的賬戶是否有充足的余額,然后支付,并且把自己所有支付的訂單保存起來。(當(dāng)然這個(gè)例子非常的簡(jiǎn)單,我們這里只是通過簡(jiǎn)單的例子展示思考的過程)
需求現(xiàn)在已經(jīng)知道了,實(shí)現(xiàn)的技術(shù)難度也不大,隨便想一下,架子基本就出來了:
傳統(tǒng)的設(shè)計(jì)方法:
大家看看上面的Customer類,很多時(shí)候,我們都是這樣的寫的(其實(shí)就是Active Record的實(shí)現(xiàn)方式,后面我們會(huì)講述企業(yè)架構(gòu)設(shè)計(jì)會(huì)談到)。
下面基本就是業(yè)務(wù)方法ProcessOrder的定義和實(shí)現(xiàn):
代碼的架子搭起來了,實(shí)現(xiàn)的思路也有了。為了確保業(yè)務(wù)的理解正確,我們可能需要跟客戶或者項(xiàng)目組的人交流,然后再編碼實(shí)現(xiàn)。在編碼的的實(shí)現(xiàn)中,該去讀數(shù)據(jù)庫的就去讀,該插入的數(shù)據(jù)的就去插入,該怎樣就怎樣。這樣代碼寫完之后,一般是調(diào)試debug(剛剛開始,為了這個(gè)功能寫個(gè)UI,不怎么劃算),看看代碼是不是按照我們的意愿在運(yùn)行。大家應(yīng)該對(duì)這種實(shí)現(xiàn)方式?jīng)]有什么意見吧。
好,現(xiàn)在在處理訂單的過程中,有加入了一些要求:如果在Order中,有產(chǎn)品的單價(jià)超過了1000的,要通知用戶一下。
代碼變?yōu)椋?/p>
public void ProcessOrder(Order order)
{
//1.獲取Customer的賬戶的余額
//2.計(jì)算Order中所有Proudct的總的價(jià)格
//3.如果有Porudct的單價(jià)超過1000,通知用戶
//4.比較 余額和 總價(jià)格
//5.保存Order信息
}
然后再調(diào)試,查詢數(shù)據(jù),插入數(shù)據(jù),deubg等等,把之前的步驟重復(fù)一下。
不知道大家現(xiàn)在是什么感覺。
在上面的例子中,在第一次的代碼實(shí)現(xiàn)中,為了判斷ProcessOrder的正確性,我們加入了數(shù)據(jù)庫的一些操作代碼。
第二次的時(shí)候只是在業(yè)務(wù)流程處理中加了一些小的改動(dòng),但是我們?cè)谡{(diào)試成本卻還是調(diào)試流程,調(diào)試數(shù)據(jù)訪問代碼。也就是說,我們第二次的時(shí)候,數(shù)據(jù)的操作方法沒有變化,變化的只是流程的處理,但是為了判斷這個(gè)ProcessOrder方法的正確性,我們還是走完了整個(gè)debug過程。
如果再次在訂單處理流程加入新的需求,那么這個(gè)方法很快膨脹起來(可能我們會(huì)把整個(gè)方法分出一些小的子方法),而且調(diào)試的成本會(huì)越來越高,而且常常重復(fù)的調(diào)試已經(jīng)功能完好的代碼,如數(shù)據(jù)訪問代碼,而且調(diào)試一次的所花的時(shí)間也越來越多。
或許有人認(rèn)為這不是個(gè)問題。因?yàn)槲遗e的例子很簡(jiǎn)單,如果在一個(gè)業(yè)務(wù)更加復(fù)雜的項(xiàng)目中很多的功能都這樣,最后的項(xiàng)目最后會(huì)怎樣?
下面我們就用TDD的設(shè)計(jì)思想來實(shí)現(xiàn)一下,然后大家自己比較:
首先,需求分析還是和之前的一樣。
下一步就要確認(rèn)需求的理解(還是和之前的一樣)。
最后開始針對(duì)需求寫測(cè)試代碼。
其實(shí)這里就有兩個(gè)問題:
1. 系統(tǒng)中哪些部分要寫測(cè)試代碼?
2. 怎么為這個(gè)需求寫測(cè)試代碼?
1. 系統(tǒng)中哪些部分要寫測(cè)試代碼?
我看過一些用TDD開發(fā)的項(xiàng)目:幾乎是每個(gè)方法都有對(duì)應(yīng)的測(cè)試代碼,而且寫的測(cè)試代碼在最后運(yùn)行的時(shí)候,測(cè)試結(jié)果居然是通過debug來看的,簡(jiǎn)直和實(shí)現(xiàn)功能代碼然后再調(diào)試沒有區(qū)別。
其實(shí)測(cè)試是有個(gè)覆蓋率的問題,覆蓋率就是:系統(tǒng)中有測(cè)試代碼的功能代碼在所有功能中的百分比。例如系統(tǒng)有100個(gè)功能,有30個(gè)功能寫了測(cè)試代碼,那么覆蓋率就是30%。
當(dāng)然100%的覆蓋率當(dāng)然好,但是也不是現(xiàn)實(shí),而且也沒有必要。一般來說要對(duì)系統(tǒng)的核心的業(yè)務(wù)流程寫測(cè)試代碼,然后再對(duì)你認(rèn)為可能會(huì)出現(xiàn)問題的地方寫一些測(cè)試代碼,用來測(cè)試如果引入變化后,這部分功能是好的。覆蓋率一般是70—80%比較合理,不過得看情況了。.
2. 怎么為這個(gè)需求寫測(cè)試代碼?
測(cè)試代碼都會(huì)寫,但是寫出好的測(cè)試代碼就不是那么容易的。首先,寫測(cè)試代碼的時(shí)候,就得站在用戶的角度,看看功能是否正確,不管內(nèi)部邏輯如何實(shí)現(xiàn)的---只看結(jié)果,不看過程的,本著這個(gè)思想來設(shè)計(jì)測(cè)試代碼。打個(gè)不恰當(dāng)?shù)谋扔鳎簻y(cè)試代碼就像是一個(gè)望子成龍,望女成鳳的家長(zhǎng),家長(zhǎng)把聰明的小孩送到學(xué)校培訓(xùn),不管怎么樣培訓(xùn),可能學(xué)校是請(qǐng)名師來教課,還是通過比賽學(xué)習(xí),還是用別的方式,家長(zhǎng)不會(huì)怎么管,最后,如果小孩成才了,那么就說明你學(xué)校有本事,不然,學(xué)校就不行。
我們開始寫測(cè)試代碼,我們開始只關(guān)注業(yè)務(wù)流程方面。
(假設(shè)沒有上面的那個(gè)類圖了,我們重新設(shè)計(jì),因?yàn)橹g的那個(gè)類圖用用來講述傳統(tǒng)的設(shè)計(jì)方式的,忘記上面的那個(gè)類圖吧)
我們的測(cè)試代碼可能會(huì)這樣寫:
public void Test_OrderProcecss_Is_Executed_Successfully()
{
Customer customer = new Customer();
Order order=new Order ();
//.....
//在Order中加入一些Product
//...
customer.ProcessOrder(order);
}
這樣編譯肯定會(huì)報(bào)錯(cuò)的:因?yàn)槲覀兿到y(tǒng)中還沒有這些類。然后我們就加上相應(yīng)的代碼的,是的編譯通過。
我們?cè)O(shè)計(jì)一個(gè)最直接的Customer類,盡量不寫多余的代碼
另外的一個(gè)問題來了:
上面的測(cè)試代碼似乎沒有反應(yīng)什么結(jié)果,到底怎么測(cè)試?
在開始寫測(cè)試的時(shí)候,會(huì)遇到這些問題。現(xiàn)在就要考慮我們之前的那個(gè)“家長(zhǎng)送孩子上學(xué)”的例子了。這里,如果系統(tǒng)訂單處理成功,那么就告訴說:OK,成功了,否則就說失敗。
測(cè)試代碼現(xiàn)在改為下面的:
public void Test_OrderProcecss_Is_Executed_Successfully()
{
Customer customer = new Customer();
Order order=new Order ();
//.....
//在Order中加入一些Product
//...
bool isSuucess=customer.ProcessOrder(order);
Assert.IsEqual(isSuucess, true);
}
OK,基本的測(cè)試代碼就這樣了。(當(dāng)然有不足的地方,我們后面跟著思考的過程慢慢的完善)
下面我們就要使得測(cè)試的代碼通過。
我們的專注先是業(yè)務(wù)流程,而不管什么數(shù)據(jù)是怎么獲取的,從哪里獲取的等,避免分散注意力。
下面我們實(shí)現(xiàn)ProcessOrder方法:
流程基本如下:
public void ProcessOrder(Order order)
{
//1.獲取Customer的賬戶的余額
//2.計(jì)算Order中所有Proudct的總的價(jià)格
//3.比較 余額和 總價(jià)格
//4.保存Order信息
}
實(shí)現(xiàn)的偽碼:
public void ProcessOrder(Order order)
{
//1.獲取Customer的賬戶的余額
decimal despoit=從一個(gè)地方獲取余額信息,不管從哪里獲取,拿來就行了。
//2.計(jì)算Order中所有Proudct的總的價(jià)格
//3.比較 余額和 總價(jià)格
//4.保存Order信息
xxx.Save(order);保存order,不管是怎么保存的,保存就行了
}
大家看到上面的代碼后,可能有點(diǎn)奇怪。因?yàn)镻rocessOrder是一個(gè)業(yè)務(wù)流程,它應(yīng)該只是關(guān)注自己的流程如何處理,如果要數(shù)據(jù),找個(gè)地方拿,要保存數(shù)據(jù),找個(gè)東西保存就行了,不管怎么查詢和怎么保存。回顧前面的“學(xué)校如何教小孩子的方法”。
現(xiàn)在有一點(diǎn)要注意:我們現(xiàn)在關(guān)注點(diǎn)是業(yè)務(wù)流程的正確性,數(shù)據(jù)從哪里來,其實(shí)不重要。
我們現(xiàn)在只是想業(yè)務(wù)流程跑通,反正測(cè)試用的數(shù)據(jù)都是我們自己設(shè)計(jì)的,即便數(shù)據(jù)如果從數(shù)據(jù)庫中來的,而且數(shù)據(jù)拿來之后,還是得放在內(nèi)存中的,何必現(xiàn)在就開始寫那么多的數(shù)據(jù)訪問代碼呢,不如直接用內(nèi)存中的數(shù)據(jù),讓流程先跑通,然后在慢慢替換數(shù)據(jù)訪問代碼。
好,既然決定數(shù)據(jù)從內(nèi)存中拿,說白了就是hard code幾個(gè)數(shù)據(jù),如果把取數(shù)據(jù)的方法還是放在Customer中,就像之前的傳統(tǒng)設(shè)計(jì)那樣。其實(shí)是有問題的:此時(shí)我們把數(shù)據(jù)訪問的代碼還是放在里面,流程通了,然后我們把hard code的代碼替換為真正的數(shù)據(jù)庫操作代碼,流程也通了。如果像之前:ProcessOrder中,加入了一個(gè)新的處理過程,我們加完代碼,運(yùn)行測(cè)試,如果測(cè)試運(yùn)行失敗了,那么此時(shí)是業(yè)務(wù)流程失敗了,還是數(shù)據(jù)訪問代碼失敗?還要debug進(jìn)行去嗎?如果還得debug,測(cè)試的代碼的作用何在?還不如一開始就不要測(cè)試,直接debug。因?yàn)榇藭r(shí)導(dǎo)致測(cè)試代碼不通過的原因有兩個(gè)了。
所以這里有一個(gè)很重要的原則:一個(gè)測(cè)試方法中,只能有一個(gè)讓它失敗的原因。不然每次運(yùn)行測(cè)試,都要debug分析,是那個(gè)原因?qū)е率 ?/p>
而且我們知道,在第二次加入新的流程過程的時(shí)候,變化的只是業(yè)務(wù)流程,其實(shí)數(shù)據(jù)訪問那塊是沒有變化的,最后我們還是打開了數(shù)據(jù)訪問代碼的所在的類,修改方法,盡管沒有修改數(shù)據(jù)訪問方法。所以這些就要把數(shù)據(jù)訪問的代碼分析出來,讓變化和不變化的獨(dú)立--—分離變化點(diǎn),萬一數(shù)據(jù)訪問代碼也變了,那就讓它們單獨(dú)的變化,這樣排錯(cuò)也好點(diǎn)。
那么一個(gè)重要的設(shè)計(jì)原則就要用上:
S--Single Responsibility Principle (SRP)
也是我們常說的”單一職責(zé)原則”。意思很好理解:每個(gè)對(duì)象有僅僅有一個(gè)讓它變化的因素,也就是說每個(gè)對(duì)象的只關(guān)注一個(gè)或者一類功能,不要把很多的不同職能的東西全部糅在一個(gè)類里面。
但是上面的類的設(shè)計(jì)嚴(yán)格的講,就是違反了SRP原則。因?yàn)樯厦娴膬蓚€(gè)職能:保存業(yè)務(wù)類的信息和負(fù)責(zé)持久化數(shù)據(jù)。
需要增加或者修改一些數(shù)據(jù)訪問的方法,那么這個(gè)類就得不斷的改動(dòng),同理,業(yè)務(wù)類的流程的變更也改變數(shù)據(jù)訪問代碼雖在的類,應(yīng)該把變化的點(diǎn)剝離出來.
用CustomerRepository來負(fù)責(zé)持久化Customer業(yè)務(wù)類的數(shù)據(jù)。這樣變化點(diǎn)就因?yàn)镾RP原則就分離了。
這樣之后,ProcessOrder方法在加了新的處理流程之后,再次運(yùn)行測(cè)試,只要測(cè)試不通過,那么可以肯定:流程代碼有問題。而且CustomerRepository隱藏?cái)?shù)據(jù)的來源,幾乎沒有變化。
其實(shí)在我們傳統(tǒng)的設(shè)計(jì)方法中,對(duì)于”單一職責(zé)”的”渴望”還不是很明顯,因?yàn)槿?果改處理流程出了問題,debug進(jìn)行看看就行了;在TDD的時(shí)候,因?yàn)榧尤肓藴y(cè)試代碼,所以把業(yè)務(wù)流程代碼和數(shù)據(jù)訪問放在一起的設(shè)計(jì)讓測(cè)試代碼”感覺”到了一點(diǎn)點(diǎn)的迷惑:是流程問題還是別的問題?所以對(duì)“單一職責(zé)”的“渴望”稍微強(qiáng)了一點(diǎn),這樣在設(shè)計(jì)時(shí)候,起碼就能夠改善一點(diǎn)點(diǎn),有點(diǎn)“驅(qū)動(dòng)好的設(shè)計(jì)”的意思。大家認(rèn)為呢?
其實(shí)”單一職責(zé)”不僅僅使用在設(shè)計(jì)類上,在設(shè)計(jì)類的方法上也有參考價(jià)值,不能把一個(gè)方法設(shè)計(jì)的N復(fù)雜。最后還要提寫有關(guān)TDD的東西:
其實(shí)上面的那個(gè)測(cè)試寫的不夠好,因?yàn)槲覀儨y(cè)試成功的情況,也要測(cè)試失敗的情況。我們不能每次都去改測(cè)試代碼去替換數(shù)據(jù)。那么我們還不如直接設(shè)計(jì)兩個(gè)測(cè)試方法,如下:
Public void Test_OrderProcecss _Executed_Successfully_With_ValidateData()
Public void Test_OrderProcecss _Executed_Failed_With_InValidateData()
我們?cè)趩卧獪y(cè)試的代碼中不要訪問數(shù)據(jù)庫,Web Service等外部的資源。例如在我們上面的CustomerRepository中,用它參與單元測(cè)試的時(shí)候,直接把數(shù)據(jù)hard code。運(yùn)行單元測(cè)試是常常要運(yùn)行的,如果用外部資源,如果因?yàn)榫W(wǎng)絡(luò)問題等導(dǎo)致測(cè)試失敗,就很容易把人搞迷惑:不清楚是功能失敗,還是其他的原因。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:網(wǎng)絡(luò)轉(zhuǎn)載