比较下面的两段代码:
直接使用第三方服务
使用封装成service的第三方服务
Angular是高度模块化的,它希望通过这种模块的形式来解决JS代码管理上的混乱,并且使用依赖注入来自动装配,这一点与SpringIOC很像,带来的好处就是你的依赖是可以随意替换的,这就极大的增加了代码的可测试性。
3、尽量将Ajax请求放到service中去做
Angular中使用service来组织那些可被复用的逻辑,除此之外,我们也可以将service理解为是对应一个领域对象的操作的集合,因此,通常会将一组Ajax操作放在一个service中去统一管理。
当然了,你也可以通过向你的directive或是controller中注入$http,但是我个人不喜欢这种做法。首先, $http是一个比较初级的依赖,与其实注入的业务服务不是一个抽象层级,如果在你的业务代码中直接操作http请求,给人的一种感觉就像是在SpringMVC的requestmethod中直接使用HttpServeletRequest一样,有点突兀,另外会让整个方法失衡,因为这些操作的抽象层次是不一样的。其次就是给测试带来的麻烦,我们不得不使用$httpBackend来模拟一个HTTP请求的发送。
我们应该设法让测试更简单,通过将Ajax请求封装到service中,我们只需要让被mock的service返回我们期望的结果就可以了。只有这样大家才会喜欢写测试,甚至是做到测试驱动开发,要去mock$http这样的东西,显然是增加了测试的负担。
4、使用Promise处理Ajax的返回值, 而不是传递回调函数
Angular中所有的Ajax请求默认都返回一个Promise对象,不建议将处理Ajax返回值的逻辑通过回调函数的形式传递给发送http请求的service,而应该是在调用service的地方利用返回的promise对象来决定如何处理。
让我们通过下面的例子来感受一下:
这里的处理办法是将快递地址验证失败或成功之后的处理函数都传给了deliveryService,当验证结果从服务器端返回之后,相应的处理函数会被执行。这做写法其实是比较常见的,但是问题出在哪里呢?
其实,作为一个service的接口, validateAddress应该只接收一个待验证的地址,验证完成之后返回一个验证结果就可以了,本来应该是一个很干净的接口,我们之所以丑陋把对应的处理函数也传进去,原因就在于这是一个异步的请求,所以需要在发请求的时候就将对处理函数绑定上去。
你应该已经猜到了第二个问题我会说一说对它的测试,通常来说,如果一个service创建成本较高或是存在外部依赖/请求的话,我们会将这个servicemock掉,通过让mockedservice直接返回我们想要的结果来让我们只关注被验证的业务逻辑。我们回头看一下上面的实现,如果我们把deliveryService的validateAddress()方法mock掉,那么我们根本没有办法去验证acceptAddress()和rejectAddress()里面的逻辑!
所以,如果你的处理函数是传递给service中的API的话,那么你的测试其实就已经跟这个API的实现绑定了,你只有去创建一个真实的service并且让它发送HTTP请求,你的处理函数才会被执行到。
经过这一番折腾,你一定要说,这测试比实现代码难写多了。正确的打开方式应该是这样的:service的API只需要返回promise,对应的处理函数的绑定在这个返回的promise上,这样我们只需要mock那个service的接口让它返回一个我们期望的promise,然后控制promise的结果让对应的处理函数被执行: