6 提交订单――用户确定她要提交订单
<o:p> </o:p>
用例中的每一段的第二部分描述了应用程序对请求作出的反应。应用程序的反应可以描述为一个职责的集合。比如说,应用程序这样执行输入发货信息的请求:
1 核实发货时间是在将来而且至少有一个餐馆提供发货信息
2 更新未提交订单的发货信息
3 显示可以提供服务的餐馆的列表
<o:p> </o:p>
应用程序的职责可以划分为两种。第一种职责是检验或者确定用户的输入,计算结果,更新数据库。一般说来services或者entities必须定义履行这个职责的方法。第二种职责是显示值。尽管有责任来显示数据,但是领域模型却是有责任来提供数据的。一般说来,entities或者repositories必须定义返回所需值的方法。每一个职责都跟一个或多个领域模型的方法有协调关系,所以实现一个职责的第一步是定义方法,然后再把这些方法分配给类。
<o:p> </o:p>
确定方法:
一旦我们决定了请求和应用程序是如何响应这些请求的,下一步就是确定领域模型类为实现上述响应而必须提供的方法。如我们在图3.1中所看到的,当应用程序处理一个请求的时候,领域模型的客户端-一个表现层或者一个facade――会一次或多次调用领域模型来验证请求,执行计算,更新数据库。它也会通过调用领域模型来得到需要显示给用户的数据。在动手写业务逻辑之前,我们必须确定领域模型客户端需要调用的方法并且决定其参数,返回类型和其所属的类。
<o:p> </o:p>
对每一个请求来说,我们一般会定义一个服务方法来做批量的工作,包括验证请求,执行计算,更新数据库。我们也要定义其他的entity和repository的方法来返回显示的数据。这些事是怎么做的呢,让我们来确定领域模型必须定义的用来处理输入发货信息请求的方法。food to go程序用两步来处理这个请求。首先,它必须检验发货信息,然后更新未提交的订单。然后,它必须显示可用餐馆的列表。
让我们考虑一下每一个职责。
<o:p> </o:p>
第一个职责是属于业务逻辑层,因为它
领域模型客户端可以直接调用PendingOrder来验证和保存发货信息。但是如我早先所提到的,一个领域模型业务(就是把业务逻辑定义在领域模型中)对处理请求来说是一个更好的选择,因为它提供更高级别的封装而且能把更多的业务逻辑移到领域模型中,这样就简化了领域模型的客户端。
<o:p> </o:p>
这个领域模型没有任何的业务,所以我们需要定义一个。最简单的做法是给place order用例定义一个叫做PlaceOrderService的业务类。它有一个updateDeliveryInfo()方法,这个方法的作用是检验是否至少有一个餐馆提供发货信息中描述的发货服务:
<o:p> </o:p>
public interface PlaceOrderService{
PlaceOrderServiceResult updateDeliveryInfo(String pendingOrderId,
Address deliveryAddress,
Date deliverTime);
}
<o:p> </o:p>
public class PlaceOrderServiceResult{
private int statusCode;
private PendingOrder pendingOrder;
}
<o:p> </o:p>
这段代码把pendingOrderId和发货信息作为参数。pendingOrderId参数是数据库中PendingOrder表的主键,而且被存储在表现层的HttpSession或者浏览器中。deliveryAddress和deliveryTime参数包含了用户输入的的值。
<o:p> </o:p>
updateDeliveryInfo()方法返回一个PlaceOrderServiceResult,它包含了一个状态码和PendingOrder。状态码表示验证发货信息是否合法的结果。这个方法返回一个PendingOrder因为调用者需要它。比如说,由表现层把它(PendingOrder)显示出来。
程序在处理输入发货信息这个请求时的另一个职责时显示提供服务餐馆的列表。这个职责主要属于表现层,因为它包含了要显示的数据。然而,领域模型必须提供一个能找到可提供该服务的餐馆的方法。找该类餐馆是一个数据库查询,该查询被repository封装。
<o:p> </o:p>
因为我们在要找到餐馆,所以添加一个RestaurantRepository到领域模型中是完全有意义的,而且要使该类有责任得到提供服务的餐馆的列表。我们定义了一个findAvailableRestaurants()方法,该方法以发货信息为参数并且返回提供发货信息中描述的服务的餐馆列表:
<o:p> </o:p>
public interface RestaurantRepository{
List findAvailableRestaurants(Address deliveryAddress,
Date deliveryTime);
}
<o:p> </o:p>
另外,因为表现层会显示餐馆的名字和类型,所以Restaurant类必须定义getter方法来返回这些值。
<o:p> </o:p>
public class Restaurant{
public String getName(){}
public String getType(){}
}
<o:p> </o:p>
getName()方法返回餐馆的名字,getType()方法返回餐馆的类型。
<o:p> </o:p>
表现层或者facade首先调用PlaceOrderService来更新pendingOrder然后调用RestaurantRepository来得到可以提供服务的餐馆。PlaceOrderService不返回提供服务餐馆的列表,因为,如果它这么做了,那么它就紧紧的和ui界面设计联系在一起了。最好是把业务逻辑与ui界面分开并且让领域模型客户端来对领域模型作额外的调用以得到需要显示的数据。facade或者表现层调用领域模型的业务逻辑处理来更新领域模型并且调用repositories来得到要显示给用户的数据。要记住:领域模型通过本地调用而被调用,而且和多重调用没有更向上层的联系。
<o:p> </o:p>
正如你所看到的那样,我们可以分析一个用例并且确定被领域模型客户端调用的方法。我们可以使用这以过程来分析place order用例的其他步骤并且确定附加的方法。一旦你已经确定了这些方法,下一步就是实现他们。
<o:p> </o:p>
用测试驱动开发来实现方法
<o:p> </o:p>
在开发过程中的这一步上,我们已经有了由PlaceOrderService和RestaurantRepository接口指定的方法而且还有一些Restaurant类的简单的getter方法。现在我们需要实现这些方法。要实现这些方法有很多途径。我最喜欢的途径是使用用例驱动开发,这是一个步正式的,以代码为中心的,发展很快的技术。当使用TDD的时候,你首先要为新功能编写自动化的单元测试用例。然后再写实现功能的代码而且要确保测试通过。
<o:p> </o:p>
举个例子,当使用TTD来实现一个业务方法比如说PlaceOrderService.updateDeliveryInfo(),你从写一个或多个测试用例开始。每一个测试使用一个特殊的参数的组合来调用方法并且验证它是否正确的更新了pendingOrder,是否返回了预期的结果。写完测试之后,你要实现业务方法并且使它们通过测试。TDD的输出正在工作,正在测试代码,自动化的测试用例。除了确保代码工作之外,测试证明了应用程序预期的行为。
<o:p> </o:p>
为了成功的使用TDD,你需要一个能为微小变化提供立即反应的开发环境。写一个测试并且使之通过这种事一天要发生很多次。重构,一个改善设计的过程,一个我们马上将要描述的过程,也由程序的小改变和测试组成。结果,每两分钟或者更少的时间内你就需要作一个编辑-编译-debug的循环动作。如果你想要高生产率,你就不能等到ejb被部署或者数据库被重建。正如你能看到的,TDD和轻量级框架技术能够很好的配合工作。
<o:p> </o:p>
重构你的代码的重要性
<o:p> </o:p>
TDD很大程度上不同于那些很多的自顶开始设计的开发技术,因为当设计不断增长的时候就需要更多的测试。但是发展一个设计的风险是你可能以一个未组织的混乱状态结束。为了阻止这种事情的发生,定期的重构代码是很重要的。
<o:p> </o:p>
重构是在不改变应用程序行为的前提下的对设计的改善的过程,一旦为新的功能准备的测试通过了重构也就完成了。重构技术的示例包括从方法种提取重复的代码,引入超类来实现通用的行为。重构代码的一个好的途径是作一系列的小变化并且每个小变化之后都要运行测试用例。重构是TDD的精华部分,它将帮助你开发出设计上很优秀的程序。
<o:p> </o:p>
使用JUnit的优点
<o:p> </o:p>
当从草稿到写测试代码成为某种可能时,这真是一个很少有的好主意。一个更好的方法是使用测试框架,如JUnit,它提供的类能够使你的开发和运行测试变得更容易。它处理异常并且报告测试失败信息;它提供使用断言的方法(JUnit提供的方法),这些断言是用来断言调用方法所得到的结果的;它能够让你把测试组织进测试套件的层次。另外,IDE,如Eclipse提供了一个GUI来运行JUnit测试。当然也有多种JUnit的扩展提供了额外的特性,如JMock,我们将在后面一点讨论它。如果要更了解JUnit,请参考JUnit in Action一书。
<o:p> </o:p>
用mock对象来简化和加速测试
<o:p> </o:p>
我是TDD的一个大粉丝而且相信:如果你想在没有混乱和花漫漫长夜来跟踪bug的情况下成功的开发软件,严格的自动化测试是要点。但是写测试是很困难的因为所有的setup代码都需要你自己写。而且,如果你写了很多的测试,他们运行起来要花很多的时间,尤其是如果他们是在访问数据库的时候。在我参与的两个项目中,当测试代码越来越多的时候,程序最终需要花30分钟才能把测试跑完。这听上去好像不是一段很长的时间,但是它是失败的一个很大的根源,因为这种失败能减慢开发速度因为每个人都需要先跑测试然后才能check in代码。
<o:p> </o:p>
为什么一个类的测试很难写而且执行速度慢呢,一个主要原因是因为它的协作者。大多数类都不是孤立的而且用一个或多个其他类来代替其协作者。举个例子,稍后,你将看到PlaceOrderService是如何来调用几个其他的领域模型类的,包括PendingOrder,RestaurantRepository,和PendingOrderRepository。协作通常是一件好事因为它能使类保持小体型。如果类需要访问外部的资源(比如说数据库)协作也是基本的,因为为了访问数据库,测试程序必须使用其他的类,比如说jdbc提供的类。但是协作可以使测试变得很困难:
<o:p> </o:p>
1 自顶向下开发测试是很难的―――你必须在编写你的任何一个类的单元测试之前实现该类的协作者。这样,比如说,就使在实现如PlaceOrderService这些类之前开发和测试一个业务逻辑类变得不可能了,而PlaceOrderService又需要在领域模型实现之前调用它。我们被强制的投入到领域模型细节的开发中去了。
<o:p> </o:p>
2 创建和初始化协作者使一个类的测试变得更加复杂―――一些对象为了测试需要复杂的初始化过程来使它们进入正确的状态。比方说,如果我们需要测试一个特定的情节,PlaceOrderService.updateDeliverInfo()被调用,以一个未提交到数据库但是已经被用户确定提交的订单id作为参数,我们将不得不调用PendingOrder中的多个方法来使之进入提交状态。这样使得写测试变得更加困难。
<o:p> </o:p>
3 协作者引入了不受欢迎的耦合
<o:p> </o:p>
举例来说,使用repositories的真实的实现将把领域模型和数据库耦合起来并且强迫我们忙于持久化上的问题。在这一点上,这样做将比我们要解决的问题还要复杂。此外,访问数据库可以减低测试的速度。
<o:p> </o:p>
<o:p> </o:p>
幸运的是,我们可以使用mock对象来解决这些问题。一个mock对象是一个假的实现以用来为测试单独地使用。一个测试将会配置一个mock对象来期待某个方法的调用并且返回预期地值。如果预期的值和实际的值不匹配的话,mock对象会抛出一个异常。使用了mock对象,我们就可以模拟领域对象的协作者而不用真的去实现他们。同时,当模拟repositories时,我们不需要去处理持久化问题而且可以写出没有数据库也可以运行的测试程序。使用mock对象允许我们简化其他复杂的对象交互,使我们可以把注意力在同一时间只集中在程序的一个部分。
<o:p> </o:p>
如果你想测试的类通过一个接口调用了一个协作者,那么实现mock对象的一个方法就是简单的定义一个实现该接口的类。对PlaceOrderService的测试用例,它使用了RestaurantRepository接口,该接口的方法应该返回测试值。尽管这样的做法在简单的测试中很有效,但是写伪装类的工作很容易变的枯燥乏味。而且,如果有一个接口需要实现你就只能使用该方法。
<o:p> </o:p>
实现mock对象的一个更好的方法是使用mock对象测试框架。他们不但使写测试变得简单,而且也支持模仿具体的类。有好几个mock对象测试框架,他们有,EasyMock,jMock。EasyMock和jMock都是JUnit框架的扩展,并且提供创建和配置mock对象的类。
<o:p> </o:p>
让我们来看看简单的例子中是如何使用jMock的,jMock是我个人最喜欢,因为它看上去比其他类似的框架更有弹性。想象一下,你需要为PlaceOrderService.updateRestaurant()方法写一个测试,需要调用RestaurantRepository.findRestaurant()方法。你不需要手工去编写一个假的RestaurantRepository类,你可以使用jMock来创建一个假的RestaurantRepository并且设定它来使之预定调用它的findRestaurant()方法。如果调用了一些其他没有预期指定要调用的方法,或者预期指定要调用的方法没有被调用,那么jMock将会抛出异常。
表3.1显示了一个PlaceOrderService类的测试用例的片断:
<o:p> </o:p>
<v:shapetype id="_x0000_t75" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" filled="f" stroked="f" coordsize="21600,21600" o:spt="75"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:connecttype="rect" o:extrusionok="f" gradientshapeok="t"></v:path><o:lock aspectratio="t" v:ext="edit"></o:lock></v:shapetype><v:shape id="_x0000_i1025" style="WIDTH: 486pt; HEIGHT: 459pt" type="#_x0000_t75"><v:imagedata o:title="l3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\01\clip_image001.jpg"></v:imagedata></v:shape>
<o:p> </o:p>
1 PlaceOrderServiceTests类继承了jMock框架提供的MockObjectTestCase类
<o:p> </o:p>
2 setUp()方法创建了假的RestaurantRepository
<o:p> </o:p>
3 得到jMock创建的代理,该代理实现了RestaurantRepository接口
<o:p> </o:p>
4 setUp()方法创建了PlaceOrderServiceImpl,把上面的代理传递给它的构造方法。
<o:p> </o:p>
5 testUpdateRestaurant_good()方法配置了假的RestaurantRepository来计划调用它的findRestaurant()方法,调用时把restaurantId传递给该方法并返回一个测试的Restaurant。
6 测试调用service方法,该方法将调用假的restaurant。
<o:p> </o:p>
jMock提供的两个关键的类是Mock和MockObjectTestCase。Mock类是用来创建一个mock对象的,这个mock的行为应该和传递给Mock()的接口或者类的实例的行为相同。一个测试用例可以通过调用proxy()方法和把返回结果看成正确的类型来访问mock对象。mock对象的预期行为可以通过调用Mock类的多种方法来定义,包括Mock.expect()方法。一个mock对象如果调用了没有预期指定要调用的方法,那么它将抛出异常。另外,一个测试用例可以检验所有预期指定要调用的方法是否已经被调用,这是通过调用Mock.verify()方法来实现的,如果预期指定要调用的方法没有被调用,该方法将抛出异常。
<o:p> </o:p>
MockObjectTestCase是JUnit类的子类,前者是用来写mock对象的测试用的。它提供了几种方便的方法来配置预期行为,包括eq(),returnValue()。此外,它会自动的在任何类型的Mock上调用verify()方法并且检验所有的预期指定要被执行的方法是否被调用了。
<o:p> </o:p>
使用一个mock对象框架(如jMock)可以让你在自顶向下的方式中从service和repository接口的方法开始来实现一个领域模型,这些方法是以需求为驱动力的。在实现了领域模型和其测试类之后,我们就已经确定了它的协作者需要实现的方法。我们可以为每一个类重复以上过程。这个过程一直重复着,直到所有的类和方法都被实现了。在这个过程的最后,我们就拥有了一个由pojo组成的可执行的可测试的领域模型了。让我们来看一下这是一个什么样的步骤。
<o:p> </o:p>
3.3 实现一个领域模型:实例
<o:p> </o:p>
在这一部分你将看到如何使用前面描述的技术来开发一个领域模型的实例。我将告诉你如何用这样的技术来实现一个方法,该方法是用来处理输入发货信息请求的。我首先实现了我们先前确定的PlaceOrderService的updateDeliveryInfo()方法。然后,我会告诉你如何实现PendingOrder的一个方法,该方法会被updateDeliveryInfo()方法调用。最后,结果是工作的而且测试PlaceOrderService和PendingOrder的方法检验了发货信息的合法性而且更新了PendingOrder。需要的repository方法也被确定下来了。学习了这个小例子之后,你将学会一个能有效的开发和测试领域模型的方法。因为repository方法需要调用持久框架,所以我们会到4-6节才实现这些方法。
<o:p> </o:p>
3.3.1实现一个领域业务方法
<o:p> </o:p>
PlaceOrderService,是一个领域模型中的业务类,有一个updateDeliveryInfo()方法,当用户输入发货信息时该方法会被调用,这个方法有如下的特点:
<o:p> </o:p>
public interface PlaceOrderService{
<o:p> </o:p>
PlaceOrderServiceResult updateDeliveryInfo(String pendingOrderId,
Address deliveryAddress,
&nbs