前言

在上一篇文章中,提到了如何通过 IoC 的设计,以及 Stub Object 的方式,来独立测试目标对象。

这一篇文章,则要说明有哪些设计对象的方式,可以让测试或需求变更时,更容易转换。

并说明这些方式有哪些特性,供读者朋友们在设计时,可以选择适合自己情境的方式来使用。

需求说明

当调用目标对象的方法时,期望目标对象的内容可以不必关注相依于哪些实体对象,而只需要依赖于某个接口,通过这样的方式来达到设计的弹性与可独立测试性。

那么,有哪一些方式可以达到这样的目的呢?

构造函数(constructor)

描述:

上一篇文章范例所使用的方式,将对象的相依接口,拉到公开的构造函数,供外部对象使用时,可自行组合目标对象的依赖对象实体。

public class Validation
{
private IAccountDao _accountDao;
private IHash _hash; public Validation(IAccountDao dao, IHash hash)
{
this._accountDao = dao;
this._hash = hash;
} public bool CheckAuthentication(string id, string password)
{
var passwordByDao = this._accountDao.GetPassword(id);
var hashResult = this._hash.GetHashResult(password); return passwordByDao == hashResult;
}
}

好处:

有许多 DI framework 支持 Autowiring。

Autowiring is an automatic detection of dependency injection points.

这里的 dependency injection points 在这例子,指的就是构造函数。以 Unity 为例,在 UnityContainer 取得目标对象时,会自动寻找目标对象参数最多的构造函数。并针对每一个参数的类型,继续在 UnityContainer 中寻找对应的实体对象,直到目标对象组合完毕,回传一个完整的目标对象。

由构造函数传入依赖接口的实体对象,是一个很通用的方式。因此在结合许多常见的 DI framework,不需要再额外处理。

顾虑点:

当对象越来越复杂时,构造函数也会趋于复杂。倘若没有 DI framework 的辅助,则使用对象上,面对许多 overload 的构造函数,或是一个构造函数的参数有好几个,会造成使用目标对象上的困难与疑惑。若没有好好进行 refactoring,也可能因此而埋藏许多 bad smell。

另外,倘若是许多构造函数,也可能造成要调用 A 方法时,应选用 A 对应的构造函数,但在使用对象上,可能会用错构造函数而不自知,若方法中没有正确的防呆,则可能出现错误。(请搭配单元测试的测试案例来辅助)

最后,与原本直接依赖的程序代码相比较,目标对象的相依对象因此暴露出来,交由外部决定,而丧失了一点封装的意味。而使用端也不一定知道,要取用此对象时,应该要注入哪些相依对象。(请使用 Repository Pattern 或 DI framework 来辅助)

公开属性(public setter property)

描述:

其实公开属性与公开构造函数非常类似,通过 public 的 property(property 类型仍为 interface),让外部在使用目标对象时,可先 setting 目标对象的相依对象,接着才调用其方法。

而公开属性通常只会将 setter 公开给外部设定,getter 则设定为 private。原因很简单,外部只需设定,而不需取用。就像公开构造函数,在使用对象之前先传入初始化对象必备的信息,但目标对象可能将这些信息,存放在 private 的 filed 或 property 中,而不需再提供给外部使用。

程序代码如下:

public class Validation
{
public IAccountDao AccountDao { private get; set; } public IHash Hash { private get; set; } public bool CheckAuthentication(string id, string password)
{
if (this.AccountDao == null)
{
throw new ArgumentNullException();
} if (this.Hash == null)
{
throw new ArgumentNullException();
} var passwordByDao = this.AccountDao.GetPassword(id);
var hashResult = this.Hash.GetHashResult(password); return passwordByDao == hashResult;
}
}

好处:

同样的,public property 也是常见的 dependency injection points,所以也有许多 DI framework 支持。另外则是不需要对构造函数进行改变,或增加新的构造函数。对过去已经存在的 legacy code 的影响,会比构造函数的方式小一点点(但几乎没有太大差异)。

顾虑点:

最常见的情况,就是使用目标对象时,依赖接口应有其对应实例,但却因为使用端没有设定 public property,导致使用方法时出现 NullReferenceException,这种情况也怪不了使用端,因为使用端极有可能本就不了解这个方法中,有哪些依赖对象。

解决方式与构造函数的建议雷同,首先当然要有测试程序来说明(测试程序就是对象使用说明书),另外取得目标对象,仍可通过 Repository Pattern,让使用端无须了解目标对象的相依关系。

并且在方法中使用依赖接口前,应检查其是否为 null,若为 null,则代表参数设定错误,进行 error handling,避免已经发生错误仍执行许多不应执行的程序代码。或是在 property 的 getter 时,检查是否为 null 或当为 null 时,给予一默认值,以避免方法无法正常执行。(视实际需求而定)

另外,公开属性的方式,也如同公开构造函数一般,破坏了一点点对象封装的用意。但这两者,都是 IoC 设计会带来的影响。

调用方法时传入参数(function parameter)

描述:

既然前面两种方式,都可能造成使用方法时,可能没有设定好依赖接口的实例,导致发生错误。或是使用目标对象时,不知道该调用哪一个构造函数或初始化哪些属性。那很简单的方式,就是把方法依赖接口的部分,拉到方法的参数上。方法中,需要使用到哪些接口,强迫由调用端必须给定参数。目标对象的方法内容则仅依赖于参数上的接口。

程序代码如下:

public bool CheckAuthentication(IAccountDao accountDao, IHash hash, string id, string password)
{
var passwordByDao = accountDao.GetPassword(id);
var hashResult = hash.GetHashResult(password); return passwordByDao == hashResult;
}

好处:

不必再担心要先初始化哪些 property,或调用哪一个构造函数。当要调用某一个方法,其相依的对象,就是得通过参数来给定。基本上也不太需要担心使用上造成困扰或迷惑。

顾虑点:

最大的问题,在于方法签名上的不稳定性。当需求异动,该方法需要额外相依于其他对象时,方法签名可能会被迫改变。而方法签章是面向对象设计上,最需要稳定的条件之一。以面向对象、接口导向设计来说,当多态对象方法签名不一致时,向来是个大问题。

另外,方法的参数过多,在使用上也会造成困扰。而且会影响到 legacy code 的调用端,需要全面跟着异动,才能编译成功。

而且通过参数的方式,DI framework 支持度较低。

但这不代表,就不能在方法参数中,传入相依对象。在 .net framework 还是有许多这样的设计,例如:List<T>.Sort 方法 (IComparer<T>)。这样的设计方式,通常要确保该方法相依相当明确、稳固,避免上述问题。

by the way, 这个方式是可以与其他方式共存的,所以在设计对象时,可衡量搭配使用。

可覆写的保护方法(protected virtual function)

描述:

前面的三种方式,基本上都对外暴露了原本可能不需要对外暴露的细节。倘若,现在的需求是眼前的程序要进行测试,但又不希望影响或修改使用端的程序,那么该怎么作呢?除了可以透过公开属性设定,当为空时给予默认值的方式,来维持原本对象的内部程序逻辑以外,还有一个相当简单的方式,甚至有些情况不需要透过接口设计,就可以进行测试。先来看看原本直接依赖对象,无法测试的程序,程序代码如下:

public class Validation
{
public bool CheckAuthentication(string id, string password)
{
var accountDao = new AccountDao();
var passwordByDao = accountDao.GetPassword(id); var hash = new Hash();
var hashResult = hash.GetHashResult(password); return passwordByDao == hashResult;
}
}

接下来,我们只用简单的面向对象概念:继承、重写,就可以对 Validation 对象的 CheckAuthentication 方法进行测试。不相信吗?继续往下看下去。

首先,一定要记得,把 new 对象的动作抽离高层抽象的 context 。(可以透过 extract method 的方式抽离)程序代码如下

public class Validation
{
public bool CheckAuthentication(string id, string password)
{
var accountDao = GetAccountDao();
var passwordByDao = accountDao.GetPassword(id); var hash = GetHash();
var hashResult = hash.GetHashResult(password); return passwordByDao == hashResult;
} private Hash GetHash()
{
var hash = new Hash();
return hash;
} private AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}
}

没什么改变,对吧?

接下来,将两个 new 对象的方法,声明为 protected virtual,代表子类别可以继承与重写该方法。程序代码如下:

protected virtual Hash GetHash()
{
var hash = new Hash();
return hash;
} protected virtual AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}

另外,将要使用到 Hash 与 AccountDao 的方法,也要声明为 virtual。程序代码如下:

public class AccountDao
{
public virtual string GetPassword(string id)
{
throw new NotImplementedException();
}
} public class Hash
{
public virtual string GetHashResult(string password)
{
throw new NotImplementedException();
}
}

到这里,都不影响外部使用目标对象的行为,我们只是在重构对象的内部方法罢了。事实上,我们可测试性的动作也准备完毕了。(当然,建议还是要依赖于接口,实现接口要顾虑的点,比继承类要轻松的多)

接下来把目光切到测试程序,该如何对 CheckAuthentication 方法进行测试。

首先,将上一篇文章的 StubHash 改为继承自 Hash,StubAccountDao 改为继承自 AccountDao,并将原本 public 的方法,加上 override 关键词,重写其父类方法内容。程序代码如下:

public class StubAccountDao : AccountDao
{
public override string GetPassword(string id)
{
return "Hello World";
}
} public class StubHash : Hash
{
public override string GetHashResult(string password)
{
return "Hello World";
}
}

不难,对吧。接下来,建立一个 MyValidation 的 class,继承自 Validation。并重写 GetAccountDao() 与 GetHash(),使其回传 Stub Object。程序代码如下:

public class MyValidation : Validation
{
protected override AccountDao GetAccountDao()
{
return new StubAccountDao();
} protected override Hash GetHash()
{
return new StubHash();
}
}

也不难,对吧。接下来,来设计单元测试,程序代码如下:

[TestMethod()]
public void CheckAuthenticationTest()
{
Validation target = new MyValidation(); string id = "id随便";
string password = "密码也随便"; bool expected = true; bool actual;
actual = target.CheckAuthentication(id, password); Assert.AreEqual(expected, actual);
}

原本初始化的测试目标为 Validation 对象,现在则为 MyValidation 对象。里面唯一不同的部分,只有重写的方法内容,其余 MyValidation 就等同于 Validation。(Is-A的关系)调试测试一下,就可以确认,程序代码就跟之前使用 IoC 的方式执行没有太大的差异。

好处:

这个方式最大的好处,是完全不影响外部使用对象的方式。仅透过 protected 与 virtual 来对继承链开放扩充的功能,并且透过这样的方式,就使得原本直接相依而导致无法测试的问题,获得解套。

顾虑点:

这是为了测试,且面对 legacy code 所使用的方式,而不是良好的面向对象设计的方式。IoC 的用意在于面向借口与扩充点的弹性,所以当可测试之后,倘若重构影响范围不大,建议读者朋友还是要将对象改依赖于接口,通过IoC 的方式来设计对象。

by the way, 同样为了解决直接相依对象,甚至相依于 static 方法、.net framework 本身的对象(如 DateTime.Now)而导致无法测试的问题,还有另外一个方式,称为 fake object。这在后面的文章,会再进行较为详尽的介绍。

结论

以上几种用来测试的方式,希望对各位读者在不同情境下的设计,可以有所帮助。

而许多延伸的议题,在这系列文章并不会多谈,但在实务应用面上,却是相当重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及通过测试程序来说明对象的使用方式,请读者在现实设计系统时,务必了解这些东西如何让系统设计更加完整。

下一篇文章,将介绍怎么样可以避免每次手工敲打这么啰唆的 stub 对象,怎么针对 static 或 .net framework 本身的对象进行隔离,怎么针对对象与相依接口互动的情况进行测试。

备注:这个系列是我毕业后时隔一年重新开始进入开发行业后对大拿们的博文摘要整理进行学习对自我的各个欠缺的方面进行充电记录博客的过程,非原创,特此感谢91 等前辈

TDD学习笔记【五】一隔绝相依性的方式与特性的更多相关文章

  1. CAS学习笔记五:SpringBoot自动/手动配置方式集成CAS单点登出

    本文目标 基于SpringBoot + Maven 分别使用自动配置与手动配置过滤器方式实现CAS客户端登出及单点登出. 本文基于<CAS学习笔记三:SpringBoot自动/手动配置方式集成C ...

  2. C#可扩展编程之MEF学习笔记(五):MEF高级进阶

    好久没有写博客了,今天抽空继续写MEF系列的文章.有园友提出这种系列的文章要做个目录,看起来方便,所以就抽空做了一个,放到每篇文章的最后. 前面四篇讲了MEF的基础知识,学完了前四篇,MEF中比较常用 ...

  3. (转)Qt Model/View 学习笔记 (五)——View 类

    Qt Model/View 学习笔记 (五) View 类 概念 在model/view架构中,view从model中获得数据项然后显示给用户.数据显示的方式不必与model提供的表示方式相同,可以与 ...

  4. java之jvm学习笔记五(实践写自己的类装载器)

    java之jvm学习笔记五(实践写自己的类装载器) 课程源码:http://download.csdn.net/detail/yfqnihao/4866501 前面第三和第四节我们一直在强调一句话,类 ...

  5. Learning ROS for Robotics Programming Second Edition学习笔记(五) indigo computer vision

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  6. Typescript 学习笔记五:类

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  7. ES6学习笔记<五> Module的操作——import、export、as

    import export 这两个家伙对应的就是es6自己的 module功能. 我们之前写的Javascript一直都没有模块化的体系,无法将一个庞大的js工程拆分成一个个功能相对独立但相互依赖的小 ...

  8. muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor

    目录 muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor Connector 系统函数connect 处理非阻塞connect的步骤: Connetor时序图 Accep ...

  9. python3.4学习笔记(五) IDLE显示行号问题,插件安装和其他开发工具介绍

    python3.4学习笔记(五) IDLE显示行号问题,插件安装和其他开发工具介绍 IDLE默认不能显示行号,使用ALT+G 跳到对应行号,在右下角有显示光标所在行.列.pycharm免费社区版.Su ...

随机推荐

  1. Oracle备库TNS连接失败的分析

    今天在测试12c的temp_undo的时候,准备在备库上测试一下,突然发现备库使用TNS连接竟然失败. 抛出的错误如下: $ sqlplus sys/oracle@testdb as sysdba S ...

  2. linux shell 用sed命令在文本的行尾或行首添加字符

    转自 http://www.cnblogs.com/aaronwxb/archive/2011/08/19/2145364.html 昨天写一个脚本花了一天的2/3的时间,而且大部分时间都耗在了sed ...

  3. http执行过程分析

    执行过程: 1.用户在浏览器(客户端)里输入或者点击一个网址链接: 2.浏览器通过网址域名查找ip地址.DNS查找方式是通过浏览器缓存(会记录DNS记录)→系统缓存→TCP/IP参数中设置的首选DNS ...

  4. 三种执行SQL语句的的JAVA代码

    问题描述: 连接数据库,执行SQL语句是必不可少的,下面给出了三种执行不通SQL语句的方法. 1.简单的Statement执行SQL语句.有SQL注入,一般不使用. public static voi ...

  5. 使用xib封装一个view的步骤

    1.新建一个xib文件描述一个view的内部结构(假设叫做SSTgCell.xib) 2.新建一个自定义的类 (自定义类需要继承自系统自带的view, 继承自哪个类,  取决于xib根对象的Class ...

  6. HTML5、微信、APP:创业寒冬只能选其一,该选哪个?

    HTML5手机网站 优势:开发技术简单,研发周期短,用户接触成本低 劣势:功能实现相比APP存在差距,用户重复使用难度大,用户粘性差 适合场景:把手机网站当成网络上的“电子产品介绍手册”.手机网站更适 ...

  7. svn 版本迁移到 git 仓库

    1.拉取 svn代码并转成 git 版本 git svn fetch http://svn.qtz.com/svn/qtz_code/java/qtz_sm/project/qtz_sm -Auser ...

  8. 2 column数据构成主键的表转化为1 column为主键的表

    问题:假设有张学生成绩表(tb)如下:姓名 课程 分数张三 语文 74张三 数学 83张三 物理 93张三 德语 null李四 语文 74李四 数学 84李四 物理 94李四 英语 80想变成(得到如 ...

  9. spark 官方文档(1)——提交应用程序

    Spark版本:1.6.2 spark-submit提供了在所有集群平台提交应用的统一接口,你不需要因为平台的迁移改变配置.Spark支持三种集群:Standalone.Apache Mesos和Ha ...

  10. JavaScript倒计时

    倒计时: 1.设置一个有效的结束日期 2.计算剩余时间 3.将时间转换成可用的格式 4.输出时钟数据作为一个可重用的对象 5.在页面上显示时钟,并在它到达0时停止 <div id="c ...