您好,登錄后才能下訂單哦!
閱讀目錄:
1.開篇介紹
2.迭代測試、重構(強制性面向接口編程,要求代碼具有可測試性)
2.1.面向接口編程的兩個設計誤區
2.1.1.接口的依賴倒置
2.1.2.接口對實體的抽象
2.2.迭代單元測試、重構(代碼可測試)
2.2.1.LINQ表達式對單元測試的影響
最近一段時間結束了一個小項目的開發,覺得有些好東西值得總結與分享,所以花點時間整理成文章;
大多數情況下我們都知道這些概念,面向接口編程是老生常談的話題了,有幾年編程經驗的都知道怎么運用;單元測試其實在前幾年不怎么被重視,然而最近逐漸的浮現在我們眼前,而且被提起的頻率也大了很多了,包括重構、可測試性都慢慢的貼近我們,我們只有親自動手去使用它才能領悟其精髓;
下面我將總結一下我對上述幾個概念之間的新體會;
面向接口編程要求我們彼此之間使用接口的方式調用,將一切可能存在變化的實例隔離在內部,這些實例都只是一個可以隨時被替換的幕后勞動者;但是面向接口編程是需要一定的設計能力,能否合理的將對象抽象出接口來,真是一句兩句話無法概括的;
面向接口設計其實本人覺得會有一些細節的設計誤區,既然抽象出接口那么就存在接口依賴的問題,還有就是對于Entity類型的抽象是否合理,是否會打亂Entity的清晰度,因為我們對DomainModel的理解是DomainEntity是一個POCO的對象,就是一個很簡單的純凈的類實體,一目了然,如果換成接口對后面的DDD的開發會有很大的麻煩,因為對接口的支持無法做到簡單的持久化,還有就是思維上的轉變也有很大的麻煩;
首先我覺得第一個誤區就是接口的依賴問題,接口的依賴不是一個小問題,在真實的項目中層之間的依賴是有嚴格的要求的,傳統分層架構要求上層只能夠依賴下層,而DDD分層架構是DomaiModel層絕對的無任何依賴,DomainModel不會去引用下層的基礎設施,因為它要求絕對的干凈;但是發現還是有很多的項目沒有能夠理解DDD的這點優點;然后就是對于層之間的實體抽取接口,其實這點真的有待商量,DataAccess Layer中的數據實體嚴格意義說是DTO對象是用來過度到Business Layer中使用的,那么如果將DataAccess中的DTO設計成接口類型對外提供使用,Business Layer 就依賴上了DataAccess Layer了,所以還是需要根據項目的具體需求來平衡,下面我們看一下示例及分析;
傳統的三層架構,在Facade中調用BLL的方法,BLL調用DAL方法,這難道不是違背了“單一職責”原則嗎;一直我們都在強調“單一職責”設計原則,為什么很多項目的每層之間都是直接使用下層的接口,特別是我們的核心DomainModel層中,本來就是很干凈的純業務處理,來一個什么數據訪問的接口真的很不美;
圖1:
這種架構應該是大部分的項目的結構,我們應該一眼就看出問題在哪里了,很明顯在Bl Layer中直接使用了Da Layer 相關接口獲取數據,單純從這一點就有點違背單一職責設計原則;
圖2:
接口依賴倒置到底是誰向誰倒置了,第一張圖是業務層依賴了數據層,詳細點就是依賴了數據訪問的接口;第二張圖中業務層沒有依賴任何東西,細心的朋友應該看到第二張圖中多了一個“DomainModel Event route ” 的東西,這是一種機制,目的是讓領域內部產生領域事件,類似事件路由的效果,基礎設施要做任何的事情跟DomaiModel Entity 本身沒有任何關系;
實體的抽象如果變成接口會很別扭,我們對實體的最直觀的認識是一個很POCO的對象,但是如果你在設計的時候將數據訪問的DTO都設計成接口是否是有點不必要,有兩個情況下可以平衡這種需要,第一如果你的DTO不需要業務層傳入數據層那么無所謂的,那么如果是需要業務層傳入數據層的接口肯定是不行的,這里就是覺得將實體與接口的概念扯到一起很不直觀,像業務實體你把它抽層接口對持久化來說就是一個問題了;
其實這篇文章的主要內容是在這一節,上一節我說了一下我對接口抽象的一點個人看法;這一節我們將通過一個具體的示例來看一下這篇文章的重要內容,看看單元測試如何與持續迭代重構完美結合的,在編寫單元測試用例的時候我們將發現代碼被逐漸的重構的很優美,面向接口編程再一次被提到一個高度;
在我們編寫代碼的時候一般情況下無法驗證我們的代碼好與壞,光憑嘴說也很難斷定每個人的設計思路是否完全正確的,所以代碼可測試性將成為驗證你所編寫的代碼的質量的一個重要指標;
單元測試與重構將是一個持續迭代的過程,很多人并不太關心重構和單元測試,其實是因為我們大部分情況下在開發一次性的交付的項目而不是持續更新的產品,所以單元測試、重構被我們所忽視,面向接口編程也被我們時而記起也時而忘記,下面我們來看一下如何編寫可測試性的代碼;
/*============================================================================== * Author:深度訓練 * Create time: 2013-08-24 * Blog Address:http://www.cnblogs.com/wangiqngpei557/ * Author Description:特定領域軟件工程實踐; *==============================================================================*/ namespace UnittestDemo { using System.Linq.Expressions; using System; public static class ServiceReport { public static Report QueryReport(string queryWhere) { return new Report(); } } }
這是一個很簡單的靜態類,主要目的是模擬根據查詢條件從服務器上查詢相關的報表信息,由于這里是為了演示所以直接返回了Report對象,只是作為實例演示,Report是作為報表對象的抽象,沒有任何的數據字段;
/*============================================================================== * Author:深度訓練 * Create time: 2013-08-24 * Blog Address:http://www.cnblogs.com/wangiqngpei557/ * Author Description:特定領域軟件工程實踐; *==============================================================================*/ namespace UnittestDemo { using System; public class ReportAnalyse { public bool Analyse(DateTime dt) { ServiceReport.QueryReport(string.Format("State={0}", 1)); return true; } } }
這是一個實例類,用來對遠程返回的表達進行分析,就好比一個業務一個數據訪問,只不過這里的數據訪問大部分情況下我們都會使用靜態類來實現;
/*============================================================================== * Author:深度訓練 * Create time: 2013-08-24 * Blog Address:http://www.cnblogs.com/wangiqngpei557/ * Author Description:特定領域軟件工程實踐; *==============================================================================*/ namespace UnittestDemo { using System; public class AppStart { public static void MainStart() { ReportAnalyse analyse = new ReportAnalyse(); bool result = analyse.Analyse(DateTime.Now); if (result) { // } else { // } } } }
這個就是程序調用的地方,用來模擬程序運行時的入口,可以當成是Application Layer中的Facade對象;
其實這里就能看出來我在2.1】小結中說的“單一職責”設計原則,我已經將數據訪問代碼在ReportAnalyse中使用了,其實這里是不對的,應該是在外部裝載好然后傳入ReportAnalyse中才對,才符合單一職責設計原則,當然這里不是講它,所以不扯了;
我們假設上面的代碼已經完成了對Report對象的分析了,下面我們需要對代碼進行單元測試,主要是兩個類ReportAnalyse、ServiceReport,我們先從ReportAnalyse類開始吧;
【單元測試】
創建基本的單元測試項目,然后記得引用被測試項目,最后新建一個用來測試ReportAnalyse類的單元測試文件;
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using UnittestDemo; namespace UnittestDemoUnit { [TestClass] public class ReportAnalyseUnitTest { [TestMethod] public void ReportAnalyse_Analyse_UnitTest() { ReportAnalyse testReportAnalyse = new ReportAnalyse(); bool result = testReportAnalyse.Analyse(DateTime.Now); Assert.IsTrue(result); } } }
寫上很簡單的測試用例,這里的主要目的不是怎么寫測試用例,也不是怎么測試代碼,這里的目的是如何進行單元測試、重構等迭代的過程,所以如何寫用例不是重點,這里直接帶過了;
圖3:
如果沒有問題的話,這個單元測試用例肯定是過的,因為沒有其他什么邏輯,很簡單的兩行代碼;看起來一起很好,沒有問題,單元測試也通過了,這個時候我們放心的去做其他的功能了,但是過了幾天發現自己的ReportAnalyse單元測試突然不過了,后來檢查發現有人改了ServiceReport實現,原本從本地直接實例化的Report現在需要配置過后才能使用,也就是說你這個時候測試不了你的代碼了,以為你的ReportAnalyse會隨時受到ServiceReport的影響,但是這個問題如果在運行時是無所謂的,畢竟在產線上都是配置好的;
這個時候就會是牽一發而動全身的困境,因為我們的代碼是面向實現編程的,也就是說耦合度很高,這個時候我們需要根據需要對ServiceReport進行適當的重構,當然重構的首要目標就是將它與任何實現脫耦;
下面我們將ServiceReport提取出一個接口,然后通過IOC的方式動態的注入進來就實現了完全的脫耦;
/*============================================================================== * Author:深度訓練 * Create time: 2013-08-24 * Blog Address:http://www.cnblogs.com/wangiqngpei557/ * Author Description:特定領域軟件工程實踐; *==============================================================================*/ namespace UnittestDemo { using System; public class ReportAnalyse { IServiceReport serviceReport; public ReportAnalyse(IServiceReport serviceReport) { this.serviceReport = serviceReport; } public bool Analyse(DateTime dt) { serviceReport.QueryReport(string.Format("State={0}", 1)); return true; } } }
這里的構造函數當然不是直接實例化的,需要使用相關的IOC框架做支撐;我們看一下上面的代碼很簡潔,依賴IServiceReport接口,這個時候我們再回過頭來對單元測試進行簡單的修改來適應可以持續重構的代碼;
為了使代碼好測試點,我修改了一下Analyse方法;
圖4:
畫紅線的部分在我們沒有進行重構之前是會隨著ServiceReport的變化而變化的,但是被我們抽象成接口之后就變的很容易測試了,我們自己可以任何控制它的返回值;
圖5:
單元測試的代碼有一點變化,從構造函數傳入的IServiceReport接口已經被Mock過了,其實這是單元測試框架的一中,.NET本身提供的Fakes框架也是很不錯的,會給出所有后臺的自動生成的模擬代碼,而且跟VisualStudioIDE是結合的,很不錯;
這個時候我們就可以控制IServiceReport接口的任何行為,我們只有將實現換成接口才能使Mock有機會插入邏輯;
按照這樣的單元測試用例,那么用例代碼是過不去的,因為我返回了一個null類型的Report對象,這里你就完全可以控制它人會的任何值,所以你的單元測試類不會受到任何外界的干擾,從而使得你的代碼具有可測試性;
到目前為止文章的中心已經講到,我們也看到一個簡單的示例,如何從面向接口編程中找到理由這么設計,其實也就是說面向接口編程就會使得類具有可測試性;單元測試與重構是一直持續下去的過程,代碼每天都有人在維護,每天都有人在使用單元測試用例,它們之間形成了一個良好的迭代關系;
圖6:
這樣持續下去代碼始終保持一個很穩定的狀態,重構過后的代碼通過單元測試進行驗證,新加入的功能也可以使用單元測試進行實時驗證;
LINQ我們用的還是蠻多的,它對于集合的處理是相當不錯的,寫起來很順手,思維也比較連貫;但是LINQ對于單元測試來說需要在編寫的時候要注意,不能過于太長,如果太長很難進行測試,就是代碼覆蓋到了也很難做到100%覆蓋率,所以如果我們有兩個嵌套以上的建議還是分成兩個獨立的方法,這樣代碼就很容易測試了,就算以后改到了也不怕會影響其他的邏輯;
一個很好的建議就是將LINQ的表達式通過方法來返回,方法里面就好比是規約一樣的工廠,將具體的LINQ表達式放入一個統一的地方管理;
總結:其實我對單元測試、重構也只是一點了解而已,只不過最近對它的理解深入了一點,所以寫出來算是對項目的一個總結,覺得還是有很大的參考價值的;任何一個新東西,在我們沒有去學習研究它的時候覺得很一般,其實真正去研究了學習了會發現真的很讓人吃驚,任何一個東西都會有存在的價值,就看我們是否需要用;很多項目包括我之前的公司長期再維護一個已經無法再維護的項目,就是因為缺乏重構、測試所以變成今天的局面,用我們公司領導的一句話說,將變成公司的“技術債務”,遲早是需要換的;其實慢慢的也就變成了公司的一個巨大的資源消耗點、累贅;
示例代碼地址:http://files.cnblogs.com/wangiqngpei557/UnittestDemo.zip
作者:王清培
出處:http://wangqingpei557.blog.51cto.com/
本文版權歸作者和51CTO共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。