Moco在程序库设计包括两个方面,如何设置服务器和如何让服务器运行起来。
先说简单的,如何让服务器运行。最简单的选择是让用户自己启动服务器,然后,在测试结束之后关闭服务器。这么一来,每个测试都大概会是这样:
- public void shouldWork() {
- ... setup server ...
- try {
- server.start();
- ... your test here ...
- } finally {
- server.stop();
- }
- }
作为一个框架,留给客户使用的接口一定简单,把实现细节封装在框架以内。如果每个测试都这么写,用户很快就会骂娘了。现在的Moco的做法是使用了running方法,把Server启停的细节封装了起来。
- running(server, new Runnable() {
- @Override
- public void run() throws Exception {
- assertThat(helper.get(root()), is("foo"));
- }
- });
我知道你不喜欢匿名内部类,我也不喜欢,但是,这是Java的局限。等到Java 8驾临,相信一切会有所改观。
设置服务器,也就是在什么样的请求,给什么样的应答。Moco有一个很关键的起点,那便是DSL,要知道,之前的一段时间我刚刚完成了《领域特定语言》的翻译。于是,API的可读性成了一件非常重要的事。
说起来很简单,但是,要知道匹配请求的条件有很多,而且还可能任意组合。如果这些条件是彼此独立的(“或”),我可以用变参来解决,但有时,这些条件是相关的(“与”),那该怎么办呢?拜函数式编程思路所赐,我想到了函数组合的方式。
我知道,Java没有一等公民的函数,但是,Java里有一等公民的对象,我们可以用对象模拟函数,事实上,这也是很多面向对象程序设计语言解决函数组合的一种方式,而这种对象称为函数对象,也叫functor。
把需要的条件封装成一个个函数对象,然后通过几个简单的运算符就可以将它们组合起来。目前Moco里提供了and和or两个运算符用于组合。
- server.request(and(by(uri("/foo")), by(method("put")))).response("bar");
- server.request(or(by("foo"), by(uri("/foo")))).response("bar");
随着面向对象思路的普及,函数组合的写法会越来越普遍的,它会成为程序员工具箱中的必备品。
DSL是一个重要的考量,所以,这里没有直接new对象,而是用函数(比如uri、method)做了一层封装。或许你会说,那为了可读性,我可以把类名设计成一个可读的样子,但new一个对象的问题在哪呢?就在new上。一方面,new是为了创建一个对象,这是实现细节,与我们要表达的内容不在一个层次,另一方面,你会发现多个new会让这句话显得不连贯。这句话?是的,我们的写法就像是用一句话声明一个东西一样,而我们期望自己讲的内容有一定的连贯性,而这才是DSL。
一旦设计成DSL,单个函数的文档也就失去了意义,更重要的是一个用法的文档。所以,在Moco里,我写了文档,却没有写JavaDoc。
在Moco里,请求和应答里可能有同样的东西,比如file,而request和response的参数类型是不一样的。如果file能够返回不同类型是***的,可惜在Java里面,我们不可能根据返回类型做重载。一种解决方案是为request和response分别设计一个函数,比如requestFile和responseFile,但显然,这种做法会把同样的逻辑分散到两个类实现里,而且这样需要共享的东西不只是file,还有其它一些东西。
在计算机问题里,加上一个中间层永远是一个重要的解决方案。这也是by的目的所在。这样一来,这些共享的东西就可以做成一个抽象(Resource),对请求来说,只要有一个by,就可以适配成Matcher。
在Moco设计中,还涉及到了一个框架设计中很重要的点:类型。Java是静态类型程序设计语言,利用好类型,就可以在编译期把错误报出来,而不会留到运行时,fail fast是很重要的一个程序设计实践。
Moco目前支持一些Content Type检测。如果把这个Content Type放到Resource里面,你会发现不是所有Resource都需要这个,比如method,我们当然可以在Resource接口里支持Content Type,在不需要的地方抛出异常。
在Moco里,我的做法是引入了一个ContentResource,支持Content Type,它继承于Resource,这样依赖,需要Content Type的接口(比如content),直接支持ContentResource,而其它部分继续支持Resource,这样,如果一不小心用错的话,编译就会报错。
再有一点是关于Publish接口,在Moco的实现里有一个Setting,还有一个BaseSetting,如果观察实现,在设置Request会创建出一个BaseSetting,但其返回的接口是Setting。这么做的一个原因是,因为Setting出现在用户可以使用的接口上,而BaseSetting是留给内部实现的,它提供了一些用于内部实现的方法,而用户是不应该使用这些方法的,所以,将二者做了一个分离,这样一来,保证了用户不会误操作。同样处理的还有HttpServer和ActualHttpServer。
小结一番,简化用户接口,设计DSL,利用好类型,区分Publish接口。