.NET專案開發—淺談面向介面程式設計、可測試性、單元測試、迭代重構(專案小結)

王清培發表於2013-08-25

閱讀目錄:

  • 1.開篇介紹 
  • 2.迭代測試、重構(強制性面向介面程式設計,要求程式碼具有可測試性)
    • 2.1.面向介面程式設計的兩個設計誤區
      • 2.1.1.介面的依賴倒置
      • 2.1.2.介面對實體的抽象 
    • 2.2.迭代單元測試、重構(程式碼可測試)
      • 2.2.1.LINQ表示式對單元測試的影響 

1】開篇介紹

最近一段時間結束了一個小專案的開發,覺得有些好東西值得總結與分享,所以花點時間整理成文章;

大多數情況下我們都知道這些概念,面向介面程式設計是老生常談的話題了,有幾年程式設計經驗的都知道怎麼運用;單元測試其實在前幾年不怎麼被重視,然而最近逐漸的浮現在我們眼前,而且被提起的頻率也大了很多了,包括重構、可測試性都慢慢的貼近我們,我們只有親自動手去使用它才能領悟其精髓;

下面我將總結一下我對上述幾個概念之間的新體會;

2】迭代測試、重構(強制性面向介面程式設計,要求程式碼具有可測試性)

【面向介面程式設計簡述】

面向介面程式設計要求我們彼此之間使用介面的方式呼叫,將一切可能存在變化的例項隔離在內部,這些例項都只是一個可以隨時被替換的幕後勞動者;但是面向介面程式設計是需要一定的設計能力,能否合理的將物件抽象出介面來,真是一句兩句話無法概括的;

面向介面設計其實本人覺得會有一些細節的設計誤區,既然抽象出介面那麼就存在介面依賴的問題,還有就是對於Entity型別的抽象是否合理,是否會打亂Entity的清晰度,因為我們對DomainModel的理解是DomainEntity是一個POCO的物件,就是一個很簡單的純淨的類實體,一目瞭然,如果換成介面對後面的DDD的開發會有很大的麻煩,因為對介面的支援無法做到簡單的持久化,還有就是思維上的轉變也有很大的麻煩;

2.1】面向介面程式設計的兩個設計誤區

首先我覺得第一個誤區就是介面的依賴問題,介面的依賴不是一個小問題,在真實的專案中層之間的依賴是有嚴格的要求的,傳統分層架構要求上層只能夠依賴下層,而DDD分層架構是DomaiModel層絕對的無任何依賴,DomainModel不會去引用下層的基礎設施,因為它要求絕對的乾淨;但是發現還是有很多的專案沒有能夠理解DDD的這點優點;然後就是對於層之間的實體抽取介面,其實這點真的有待商量,DataAccess Layer中的資料實體嚴格意義說是DTO物件是用來過度到Business Layer中使用的,那麼如果將DataAccess中的DTO設計成介面型別對外提供使用,Business Layer 就依賴上了DataAccess Layer了,所以還是需要根據專案的具體需求來平衡,下面我們看一下示例及分析;

2.1.1】 介面的依賴倒置

傳統的三層架構,在Facade中呼叫BLL的方法,BLL呼叫DAL方法,這難道不是違背了“單一職責”原則嗎;一直我們都在強調“單一職責”設計原則,為什麼很多專案的每層之間都是直接使用下層的介面,特別是我們的核心DomainModel層中,本來就是很乾淨的純業務處理,來一個什麼資料訪問的介面真的很不美;

圖1:

這種架構應該是大部分的專案的結構,我們應該一眼就看出問題在哪裡了,很明顯在Bl Layer中直接使用了Da Layer 相關介面獲取資料,單純從這一點就有點違背單一職責設計原則;

圖2:

介面依賴倒置到底是誰向誰倒置了,第一張圖是業務層依賴了資料層,詳細點就是依賴了資料訪問的介面;第二張圖中業務層沒有依賴任何東西,細心的朋友應該看到第二張圖中多了一個“DomainModel Event route ” 的東西,這是一種機制,目的是讓領域內部產生領域事件,類似事件路由的效果,基礎設施要做任何的事情跟DomaiModel Entity 本身沒有任何關係;

2.1.2】 介面對實體的抽象

實體的抽象如果變成介面會很彆扭,我們對實體的最直觀的認識是一個很POCO的物件,但是如果你在設計的時候將資料訪問的DTO都設計成介面是否是有點不必要,有兩個情況下可以平衡這種需要,第一如果你的DTO不需要業務層傳入資料層那麼無所謂的,那麼如果是需要業務層傳入資料層的介面肯定是不行的,這裡就是覺得將實體與介面的概念扯到一起很不直觀,像業務實體你把它抽層介面對持久化來說就是一個問題了;

2.2】迭代單元測試、重構(程式碼可測試)

其實這篇文章的主要內容是在這一節,上一節我說了一下我對介面抽象的一點個人看法;這一節我們將通過一個具體的示例來看一下這篇文章的重要內容,看看單元測試如何與持續迭代重構完美結合的,在編寫單元測試用例的時候我們將發現程式碼被逐漸的重構的很優美,面向介面程式設計再一次被提到一個高度;

在我們編寫程式碼的時候一般情況下無法驗證我們的程式碼好與壞,光憑嘴說也很難斷定每個人的設計思路是否完全正確的,所以程式碼可測試性將成為驗證你所編寫的程式碼的質量的一個重要指標;

單元測試與重構將是一個持續迭代的過程,很多人並不太關心重構和單元測試,其實是因為我們大部分情況下在開發一次性的交付的專案而不是持續更新的產品,所以單元測試、重構被我們所忽視,面向介面程式設計也被我們時而記起也時而忘記,下面我們來看一下如何編寫可測試性的程式碼;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-24
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7  
 8 namespace UnittestDemo
 9 {
10     using System.Linq.Expressions;
11     using System;
12  
13     public static class ServiceReport
14     {
15         public static Report QueryReport(string queryWhere)
16         {
17             return new Report();
18         }
19     }
20 }
View Code

這是一個很簡單的靜態類,主要目的是模擬根據查詢條件從伺服器上查詢相關的報表資訊,由於這裡是為了演示所以直接返回了Report物件,只是作為例項演示,Report是作為報表物件的抽象,沒有任何的資料欄位;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-24
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7  
 8 namespace UnittestDemo
 9 {
10     using System;
11  
12     public class ReportAnalyse
13     {
14         public bool Analyse(DateTime dt)
15         {
16             ServiceReport.QueryReport(string.Format("State={0}", 1));
17             return true;
18         }
19     }
20 }
View Code

這是一個例項類,用來對遠端返回的表達進行分析,就好比一個業務一個資料訪問,只不過這裡的資料訪問大部分情況下我們都會使用靜態類來實現;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-24
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7  
 8 namespace UnittestDemo
 9 {
10     using System;
11     public class AppStart
12     {
13         public static void MainStart()
14         {
15             ReportAnalyse analyse = new ReportAnalyse();
16             bool result = analyse.Analyse(DateTime.Now);
17  
18             if (result)
19             {
20                 //
21             }
22             else
23             {
24                 //
25             }
26         }
27     }
28 }
View Code

這個就是程式呼叫的地方,用來模擬程式執行時的入口,可以當成是Application Layer中的Facade物件;

其實這裡就能看出來我在2.1】小結中說的“單一職責”設計原則,我已經將資料訪問程式碼在ReportAnalyse中使用了,其實這裡是不對的,應該是在外部裝載好然後傳入ReportAnalyse中才對,才符合單一職責設計原則,當然這裡不是講它,所以不扯了;

我們假設上面的程式碼已經完成了對Report物件的分析了,下面我們需要對程式碼進行單元測試,主要是兩個類ReportAnalyse、ServiceReport,我們先從ReportAnalyse類開始吧;

【單元測試】

建立基本的單元測試專案,然後記得引用被測試專案,最後新建一個用來測試ReportAnalyse類的單元測試檔案;

 1 using System;
 2 using Microsoft.VisualStudio.TestTools.UnitTesting;
 3 using UnittestDemo;
 4  
 5 namespace UnittestDemoUnit
 6 {
 7     [TestClass]
 8     public class ReportAnalyseUnitTest
 9     {
10         [TestMethod]
11         public void ReportAnalyse_Analyse_UnitTest()
12         {
13             ReportAnalyse testReportAnalyse = new ReportAnalyse();
14             bool result = testReportAnalyse.Analyse(DateTime.Now);
15  
16             Assert.IsTrue(result);
17         }
18     }
19 }
View Code

寫上很簡單的測試用例,這裡的主要目的不是怎麼寫測試用例,也不是怎麼測試程式碼,這裡的目的是如何進行單元測試、重構等迭代的過程,所以如何寫用例不是重點,這裡直接帶過了;

圖3:

如果沒有問題的話,這個單元測試用例肯定是過的,因為沒有其他什麼邏輯,很簡單的兩行程式碼;看起來一起很好,沒有問題,單元測試也通過了,這個時候我們放心的去做其他的功能了,但是過了幾天發現自己的ReportAnalyse單元測試突然不過了,後來檢查發現有人改了ServiceReport實現,原本從本地直接例項化的Report現在需要配置過後才能使用,也就是說你這個時候測試不了你的程式碼了,以為你的ReportAnalyse會隨時受到ServiceReport的影響,但是這個問題如果在執行時是無所謂的,畢竟在產線上都是配置好的;

這個時候就會是牽一髮而動全身的困境,因為我們的程式碼是面向實現程式設計的,也就是說耦合度很高,這個時候我們需要根據需要對ServiceReport進行適當的重構,當然重構的首要目標就是將它與任何實現脫耦;

下面我們將ServiceReport提取出一個介面,然後通過IOC的方式動態的注入進來就實現了完全的脫耦;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-24
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7  
 8 namespace UnittestDemo
 9 {
10     using System;
11  
12     public class ReportAnalyse
13     {
14         IServiceReport serviceReport;
15  
16         public ReportAnalyse(IServiceReport serviceReport)
17         {
18             this.serviceReport = serviceReport;
19         }
20  
21         public bool Analyse(DateTime dt)
22         {
23             serviceReport.QueryReport(string.Format("State={0}", 1));
24             return true;
25         }
26     }
27 }
View Code

這裡的建構函式當然不是直接例項化的,需要使用相關的IOC框架做支撐;我們看一下上面的程式碼很簡潔,依賴IServiceReport介面,這個時候我們再回過頭來對單元測試進行簡單的修改來適應可以持續重構的程式碼;

為了使程式碼好測試點,我修改了一下Analyse方法;

圖4:

畫紅線的部分在我們沒有進行重構之前是會隨著ServiceReport的變化而變化的,但是被我們抽象成介面之後就變的很容易測試了,我們自己可以任何控制它的返回值;

圖5:

單元測試的程式碼有一點變化,從建構函式傳入的IServiceReport介面已經被Mock過了,其實這是單元測試框架的一中,.NET本身提供的Fakes框架也是很不錯的,會給出所有後臺的自動生成的模擬程式碼,而且跟VisualStudioIDE是結合的,很不錯;

這個時候我們就可以控制IServiceReport介面的任何行為,我們只有將實現換成介面才能使Mock有機會插入邏輯;

按照這樣的單元測試用例,那麼用例程式碼是過不去的,因為我返回了一個null型別的Report物件,這裡你就完全可以控制它人會的任何值,所以你的單元測試類不會受到任何外界的干擾,從而使得你的程式碼具有可測試性;

到目前為止文章的中心已經講到,我們也看到一個簡單的示例,如何從面向介面程式設計中找到理由這麼設計,其實也就是說面向介面程式設計就會使得類具有可測試性;單元測試與重構是一直持續下去的過程,程式碼每天都有人在維護,每天都有人在使用單元測試用例,它們之間形成了一個良好的迭代關係;

圖6:

這樣持續下去程式碼始終保持一個很穩定的狀態,重構過後的程式碼通過單元測試進行驗證,新加入的功能也可以使用單元測試進行實時驗證;

2.2.1】LINQ表示式對單元測試的影響

LINQ我們用的還是蠻多的,它對於集合的處理是相當不錯的,寫起來很順手,思維也比較連貫;但是LINQ對於單元測試來說需要在編寫的時候要注意,不能過於太長,如果太長很難進行測試,就是程式碼覆蓋到了也很難做到100%覆蓋率,所以如果我們有兩個巢狀以上的建議還是分成兩個獨立的方法,這樣程式碼就很容易測試了,就算以後改到了也不怕會影響其他的邏輯;

一個很好的建議就是將LINQ的表示式通過方法來返回,方法裡面就好比是規約一樣的工廠,將具體的LINQ表示式放入一個統一的地方管理;

 

總結:其實我對單元測試、重構也只是一點了解而已,只不過最近對它的理解深入了一點,所以寫出來算是對專案的一個總結,覺得還是有很大的參考價值的;任何一個新東西,在我們沒有去學習研究它的時候覺得很一般,其實真正去研究了學習了會發現真的很讓人吃驚,任何一個東西都會有存在的價值,就看我們是否需要用;很多專案包括我之前的公司長期再維護一個已經無法再維護的專案,就是因為缺乏重構、測試所以變成今天的局面,用我們公司領導的一句話說,將變成公司的“技術債務”,遲早是需要換的;其實慢慢的也就變成了公司的一個巨大的資源消耗點、累贅;

 

示例程式碼地址:http://files.cnblogs.com/wangiqngpei557/UnittestDemo.zip

 

相關文章