天天看點

一起談.NET技術,走向ASP.NET架構設計——第二章:設計/ 測試/代碼

  再次申明一下:本系列不是講述TDD的,隻是用TDD來建立設計的思想。即便是用DDD,有時候還是結合TDD一起使用的。

  開發方式比較

  我們用下面的一段分析來引出今天的内容:

  想想我們平時是如何在寫代碼:拿來需求,分析功能,編寫功能代碼。這樣的方式,沒有問題,大家也一直沿用很多年了。為了後面描述友善,我們稱這種方式為傳統流程。

  TDD的怎麼做的:

  拿來需求,分析功能,寫功能測試代碼,編寫功能代碼。其實兩個過程差不多的,真的差不多的。

  首先來分析下兩種開發流程。個人認為:因為TDD多了一個角色轉換的過程:在我們傳統流程中,我們一直以一個開發人員的思維在想問題,分析,然後就開始實作。在TDD中,在分析功能之後,我們就要站在客戶的角度(當然很多時候還是我們自己在模拟客戶)就要檢測這個功能是不是真正需要的,然後在這個前提下,再開始編碼。

  下面我們再來看一組分析圖:

一起談.NET技術,走向ASP.NET架構設計——第二章:設計/ 測試/代碼

  因為從拿到需求和了解需求,到最後的實作,這個過程肯定是有偏差的。就如上圖。

一起談.NET技術,走向ASP.NET架構設計——第二章:設計/ 測試/代碼

  在TDD中,在功能測試那一個環節,就把這種偏差控制了起來。即使最後有偏差,但是小了一些。

  為什麼要将兩種開發的方式比較?

  首先,從總體上來看,傳統的流程就是先做出基本有用的東西,而且TDD先是搭個架子,然後在做東西。

  在TDD中,我們是直奔功能:針對需求出測試,然後針對測試出功能。一針見血。可能這些功能暫時還不能完全用,因為缺少東西,如資料庫,在測試中我們可能是模拟的。例如,在實作一個功能的時候,如果這個功能需要操作資料庫或者要通過網絡通路,那麼我們在用傳統的方法寫的時候,想要看看功能最後實作的效果,往往是debug,或者做出可視化的東西出來,注意力很快就被分散了,如果發現需求了解不對,之前的就重新來過,代價可能而知。而采用TDD的方法,可以先寫測試模拟,如用mock, stub等,這樣關注點主要在業務上,這種方式就好比水波效應:從中心向周圍擴散。

  什麼是設計 

  一個軟體系統,最重要的就是核心業務功能,系統設計的時候,肯定先是分析功能,并且确認分析的功能是符合需求的,然後再為實作功能尋找解決方案。在有了解決方案的前提下,再考慮上技術的選擇,複雜性,可擴充行,可維護性,可行性等,最後就”設計”就産生了,确定實作方案之後,最後實作。”設計”确确實實是一個腦力活。

  那麼我們就來看看,如何做出一個比較好的設計。做設計,考慮的太多,太少都不行。多則可能“過度”,少則可能不全。

  我們下面就用TDD來幫助我們建立一些設計的思想。

  在此之前,有一點我想提出:TDD不是測試,而是設計。如果之前一直以為TDD就是寫測試,那麼就說明對TDD的了解還在“形”上。

  設計初探

  我們之前說過:TDD不是測試,更多的是設計的思路。那麼為什麼在寫代碼之前寫測試可以有個比較好的設計?我們就來體驗一下。

  我們知道,在面向對象的設計中,有很多的設計原則,例如S.O.L.I.D,在系統中充分的使用這些原則,會導緻一個良性的開發過程。是以一個比較的好的設計,應該是盡量的向這些設計原則上面靠攏的。

  看一個例子:

  例如在使用者訂單管理系統中有一個需求:客戶在下訂單的時候首先要去看看自己的賬戶是否有充足的餘額,然後支付,并且把自己所有支付的訂單儲存起來。(當然這個例子非常的簡單,我們這裡隻是通過簡單的例子展示思考的過程)

  需求現在已經知道了,實作的技術難度也不大,随便想一下,架子基本就出來了:

  傳統的設計方法:

  大家看看上面的Customer類,很多時候,我們都是這樣的寫的(其實就是Active Record的實作方式,後面我們會講述企業架構設計會談到)。

  下面基本就是業務方法ProcessOrder的定義和實作:

public void ProcessOrder(Order order)

{

//1.擷取 Customer的賬戶的餘額

//2.計算Order中所有Proudct的總的價格

//3. 比較 餘額和 總價格

//4.儲存Order資訊

}

  代碼的架子搭起來了,實作的思路也有了。為了確定業務的了解正确,我們可能需要跟客戶或者項目組的人交流,然後再編碼實作。在編碼的的實作中,該去讀資料庫的就去讀,該插入的資料的就去插入,該怎樣就怎樣。這樣代碼寫完之後,一般是調試debug(剛剛開始,為了這個功能寫個UI,不怎麼劃算),看看代碼是不是按照我們的意願在運作。大家應該對這種實作方式沒有什麼意見吧。

  好,現在在處理訂單的過程中,有加入了一些要求:如果在Order中,有産品的單價超過了1000的,要通知使用者一下。

  代碼變為:

//3. 如果有Porudct的單價超過1000,通知使用者

//4.比較 餘額和 總價格

//5. 儲存Order資訊

  然後再調試,查詢資料,插入資料,deubg等等,把之前的步驟重複一下。不知道大家現在是什麼感覺。

  在上面的例子中,在第一次的代碼實作中,為了判斷ProcessOrder的正确性,我們加入了資料庫的一些操作代碼。 

  第二次的時候隻是在業務流程進行中加了一些小的改動,但是我們在調試成本卻還是調試流程,調試資料通路代碼。也就是說,我們第二次的時候,資料的操作方法沒有變化,變化的隻是流程的處理,但是為了判斷這個ProcessOrder方法的正确性,我們還是走完了整個debug過程。

  如果再次在訂單處理流程加入新的需求,那麼這個方法很快膨脹起來(可能我們會把整個方法分出一些小的子方法),而且調試的成本會越來越高,而且常常重複的調試已經功能完好的代碼,如資料通路代碼,而且調試一次的所花的時間也越來越多。

   或許有人認為這不是個問題。因為我舉的例子很簡單,如果在一個業務更加複雜的項目中很多的功能都這樣,最後的項目最後會怎樣?

  下面我們就用TDD的設計思想來實作一下,然後大家自己比較:

  首先,需求分析還是和之前的一樣。下一步就要确認需求的了解(還是和之前的一樣)。最後開始針對需求寫測試代碼。

  其實這裡就有兩個問題:

  1. 系統中哪些部分要寫測試代碼?

   我看過一些用TDD開發的項目:幾乎是每個方法都有對應的測試代碼,而且寫的測試代碼在最後運作的時候,測試結果居然是通過debug來看的,簡直和實作功能代碼然後再調試沒有差別。

   其實測試是有個覆寫率的問題,覆寫率就是:系統中有測試代碼的功能代碼在所有功能中的百分比。例如系統有100個功能,有30個功能寫了測試代碼,那麼覆寫率就是30%。

當然100%的覆寫率當然好,但是也不是現實,而且也沒有必要。一般來說要對系統的核心的業務流程寫測試代碼,然後再對你認為可能會出現問題的地方寫一些測試代碼,用來測試如果引入變化後,這部分功能是好的。覆寫率一般是70—80%比較合理,不過得看情況了。

  2.怎麼為這個需求寫測試代碼?

  測試代碼都會寫,但是寫出好的測試代碼就不是那麼容易的。首先,寫測試代碼的時候,就得站在使用者的角度,看看功能是否正确,不管内部邏輯如何實作的---隻看結果,不看過程的,本着這個思想來設計測試代碼。打個不恰當的比喻:測試代碼就像是一個望子成龍,望女成鳳的家長,家長把聰明的小孩送到學校教育訓練,不管怎麼樣教育訓練,可能學校是請名師來教課,還是通過比賽學習,還是用别的方式,家長不會怎麼管,最後,如果小孩成才了,那麼就說明你學校有本事,不然,學校就不行。

  我們開始寫測試代碼,我們開始隻關注業務流程方面。(假設沒有上面的那個類圖了,我們重新設計,因為之間的那個類圖用用來講述傳統的設計方式的,忘記上面的那個類圖吧)

  我們的測試代碼可能會這樣寫:

public void Test_OrderProcecss_Is_Executed_Successfully()

Customer customer = new Customer();

Order order=new Order ();

//.....

// 在Order中加入一些Product

//...

customer.ProcessOrder(order);

  這樣編譯肯定會報錯的:因為我們系統中還沒有這些類。然後我們就加上相應的代碼的,是的編譯通過。我們設計一個最直接的Customer類,盡量不寫多餘的代碼:

  另外的一個問題來了:

  上面的測試代碼似乎沒有反應什麼結果,到底怎麼測試?在開始寫測試的時候,會遇到這些問題。現在就要考慮我們之前的那個“家長送孩子上學”的例子了。這裡,如果系統訂單處理成功,那麼就告訴說:OK,成功了,否則就說失敗。測試代碼現在改為下面的:

bool isSuucess=customer.ProcessOrder(order);

Assert.IsEqual(isSuucess, true);

  OK,基本的測試代碼就這樣了。(當然有不足的地方,我們後面跟着思考的過程慢慢的完善)

  下面我們就要使得測試的代碼通過。我們的專注先是業務流程,而不管什麼資料是怎麼擷取的,從哪裡擷取的等,避免分散注意力。

  下面我們實作ProcessOrder方法:

  流程基本如下:

   實作的僞碼: 

//1.擷取Customer的賬戶的餘額

decimal despoit=從一個地方擷取餘額資訊,不管從哪裡擷取,拿來就行了。

//3.比較 餘額和 總價格

xxx.Save(order); 儲存order,不管是怎麼儲存的,儲存就行了

  大家看到上面的代碼後,可能有點奇怪。因為ProcessOrder是一個業務流程,它應該隻是關注自己的流程如何處理,如果要資料,找個地方拿,要儲存資料,找個東西儲存就行了,不管怎麼查詢和怎麼儲存。回顧前面的“學校如何教小孩子的方法”。

  現在有一點要注意:我們現在關注點是業務流程的正确性,資料從哪裡來,其實不重要。

  我們現在隻是想業務流程跑通,反正測試用的資料都是我們自己設計的,即便資料如果從資料庫中來的,而且資料拿來之後,還是得放在記憶體中的,何必現在就開始寫那麼多的資料通路代碼呢,不如直接用記憶體中的資料,讓流程先跑通,然後在慢慢替換資料通路代碼。

  好,既然決定資料從記憶體中拿,說白了就是hard code幾個資料,如果把取資料的方法還是放在Customer中,就像之前的傳統設計那樣。其實是有問題的:此時我們把資料通路的代碼還是放在裡面,流程通了,然後我們把hard code的代碼替換為真正的資料庫操作代碼,流程也通了。如果像之前:ProcessOrder中,加入了一個新的處理過程,我們加完代碼,運作測試,如果測試運作失敗了,那麼此時是業務流程失敗了,還是資料通路代碼失敗?還要debug進行去嗎?如果還得debug,測試的代碼的作用何在?還不如一開始就不要測試,直接debug。因為此時導緻測試代碼不通過的原因有兩個了。

  是以這裡有一個很重要的原則:一個測試方法中,隻能有一個讓它失敗的原因。不然每次運作測試,都要debug分析,是那個原因導緻失敗。

  而且我們知道,在第二次加入新的流程過程的時候,變化的隻是業務流程,其實資料通路那塊是沒有變化的,最後我們還是打開了資料通路代碼的所在的類,修改方法,盡管沒有修改資料通路方法。是以這些就要把資料通路的代碼分析出來,讓變化和不變化的獨立--—分離變化點,萬一資料通路代碼也變了,那就讓它們單獨的變化,這樣排錯也好點。

  那麼一個重要的設計原則就要用上:

  S--Single Responsibility Principle (SRP)

  也是我們常說的”單一職責原則”。意思很好了解:每個對象有僅僅有一個讓它變化的因素,也就是說每個對象的隻關注一個或者一類功能,不要把很多的不同職能的東西全部糅在一個類裡面。 

  但是上面的類的設計嚴格的講,就是違反了SRP原則。因為上面的兩個職能:儲存業務類的資訊和負責持久化資料。

  需要增加或者修改一些資料通路的方法,那麼這個類就得不斷的改動,同理,業務類的流程的變更也改變資料通路代碼雖在的類,應該把變化的點剝離出來.

  用CustomerRepository來負責持久化Customer業務類的資料。這樣變化點就因為SRP原則就分離了。 

  這樣之後,ProcessOrder方法在加了新的處理流程之後,再次運作測試,隻要測試不通過,那麼可以肯定:流程代碼有問題。而且CustomerRepository隐藏資料的來源,幾乎沒有變化。

  其實在我們傳統的設計方法中,對于”單一職責”的”渴望”還不是很明顯,因為如果改處理流程出了問題,debug進行看看就行了;在TDD的時候,因為加入了測試代碼,是以把業務流程代碼和資料通路放在一起的設計讓測試代碼”感覺”到了一點點的迷惑:是流程問題還是别的問題?是以對“單一職責”的“渴望”稍微強了一點,這樣在設計時候,起碼就能夠改善一點點,有點“驅動好的設計”的意思。大家認為呢?

  其實”單一職責”不僅僅使用在設計類上,在設計類的方法上也有參考價值,不能把一個方法設計的N複雜。最後還要提寫有關TDD的東西:

  其實上面的那個測試寫的不夠好,因為我們測試成功的情況,也要測試失敗的情況。我們不能每次都去改測試代碼去替換資料。那麼我們還不如直接設計兩個測試方法,如下:

  Public void Test_OrderProcecss _Executed_Successfully_With_ValidateData()

  Public void Test_OrderProcecss _Executed_Failed_With_InValidateData()

  我們在單元測試的代碼中不要通路資料庫,Web Service等外部的資源。例如在我們上面的CustomerRepository中,用它參與單元測試的時候,直接把資料hard code。運作單元測試是常常要運作的,如果用外部資源,如果因為網絡問題等導緻測試失敗,就很容易把人搞迷惑:不清楚是功能失敗,還是其他的原因。

  具體的我們以後再講述吧!

  我是希望盡量把思考的過程通俗的講出來,是以顯得啰啰嗦嗦的!不知道大家是什麼感受!希望大家回報!