Fork me on GitHub

.NET项目开发—浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)

阅读目录:

  • 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表达式放入一个统一的地方管理;

 

总结:其实我对单元测试、重构也只是一点了解而已,只不过最近对它的理解深入了一点,所以写出来算是对项目的一个总结,觉得还是有很大的参考价值的;任何一个新东西,在我们没有去学习研究它的时候觉得很一般,其实真正去研究了学习了会发现真的很让人吃惊,任何一个东西都会有存在的价值,就看我们是否需要用;很多项目包括我之前的公司长期再维护一个已经无法再维护的项目,就是因为缺乏重构、测试所以变成今天的局面,用我们公司领导的一句话说,将变成公司的“技术债务”,迟早是需要换的;其实慢慢的也就变成了公司的一个巨大的资源消耗点、累赘;

 

示例代码地址:https://files.cnblogs.com/wangiqngpei557/UnittestDemo.zip

 

posted @ 2013-08-25 13:06  王清培  阅读(5100)  评论(12编辑  收藏  举报