该系列第1篇: 讲述了如何创造"缝". "缝"(seam)是须要知道的概念.html
本文是第2篇, 介绍的是如何避免在构建对象时写出不易测试的代码. 本文的概念性内容大部分都来自Misko Hevery的这篇博客文章.ide
仍是用上文里汽车的例子.函数
一般状况下, 咱们是先去建造汽车, 组装好汽车后, 咱们再去驾驶它.测试
软件开发也相似, 咱们应该把对象构造完毕以后, 再去用它. 可是有时候, 开发者会在构造过程当中添加一些程序逻辑. 这就至关于车还没造完, 咱们就驾驶它去兜风了. 这样作是不太好的.ui
构造函数是类用来建立其实例对象的方法, 这里的代码是用来准备该对象的. 但有时开发者会在构造函数里作一些其它的工做, 例如构建依赖项, 执行初始化逻辑等等.spa
在构造函数(或者更大一点, 指构建的过程)里, 作这些额外的工做会让测试变得异常困难. 这是由于像初始化依赖项, 调用服务, 设置状态的逻辑等这些工做会把用于测试的"缝"弄丢. 致使没法进行mock.3d
总之在构造的过程当中作太多的工做会妨碍测试.code
总之就是要避免对象的构建和对象的行为混合到一块儿, 由于它们在一块儿就会很难进行测试.htm
最后还有一点, 首先你须要知道, 根据angular的创始人Misko Hevery所说:对象
对象的构造分两类, 一种是可注入的, 一种是可new的.
可注入的对象能够由其它的一堆可注入对象组成. 它们能够为 可new的 对象工做. 可注入的对象一般是实现了接口的service, 像什么IUnitOfWork, IRepository, IxxxService等等.
可new的对象就是对象图里的终点, 例如实体或者值对象(Value Object)等.
为了易于测试, 针对这两类构造, 有下列规则:
可注入的对象能够在构造函数请求(注入)其它的能够注入对象, 可是不能在构造函数请求可new的对象.
反过来, 可new的对象能够在构造函数请求其它的可new对象, 可是不能在构造函数请求可注入的对象.
这是不对的, 构建的过程当中直接new的话, 就会形成紧耦合, 也没法在测试中使用Test Double来代替它们了. 若是测试中不代替它们的话, 有些服务的开销可能会很大.
正确的写法是使用依赖注入:
该例中, UserController只须要UserService和LoggingService两个依赖项. 可是UserService又依赖于UserRepository.
可是这样写就不对了, 这会形成UserController和UserRepository间的紧耦合, 并且配置UserService也并非UserController的责任.
正确的写法是:
而UserService也最好是注入依赖.
而若是UserService并非在构造函数注入UserRepository的话:
那么Controller里就应该这样写:
不过最好仍是使用构造函数注入的写法.
仔细的说, 该例有不止一处错误.
首先它有条件判断逻辑代码; 此外它还使用了ApplicationState.IsRunning这个静态变量(就是全局状态); 并且在构造函数里还作了UserService的配置工做, 这不是UserController的责任.
尽可能要避免全局变量, 它没法进行隔离, 测试会遇到麻烦, 例如并行测试时其中一个测试改变了静态变量的值就可能致使另外一个测试失败.
可是粗略的说, 该例能够说就是一个错误, 如何配置UserService并非UserController的责任, 因此, 正确的作法是把UserService配置相关的代码移出去, 让它本身去管理吧:
该例子中, LoggingService的Log方法须要一个Area类型的对象, 它是一个值对象.
因此它的错误就是, 不该该把可new的对象注入到可注入的对象里. 这么作的话, 测试就很差作隔离了.
正确的作法应该是, 做为方法的参数传递进来:
若是出现类相似initalize()或相似意思的方法, 颇有可能说明该对象的责任太多了.
修改它很简单, 让各自的类负责本身的内容便可. 去掉initialize()方法便可.
例子就举这些, 并不全, 详细请看Angular做者的博文.
上面例子里的UserController就是咱们须要使用的对象, 在运行时, 代码多是这样的:
构建这个对象仍是有点麻烦的, 它的类关系图以下:
因此测试的设置过程也会比较麻烦:
固然也能够不直接new, 而是使用mock. 总之都很麻烦.
因此咱们可使用Factory等模式, 把构建UserController的工做放到工厂里:
能够这样调用:
若是项目使用了IoC容器的话, 还可使用相似下面的用法:
先介绍到这里.