EJB Command
一个ejb客户端为了完成一个用例需要执行商业逻辑。
怎样让一个开发者用一个轻量级的态度实现一个用例的
商业逻辑,使客户端和ejb解耦并且用一个事务和一次
网络调用执行用例?
设计ejb系统时的一个重要的架构决定是把商业逻辑
放到什么地方?一个用例的商业逻辑是代表你的领域
模型中的合适的方法或跨多个其它entity bean 和/或
session bean执行逻辑(工作流逻辑)。
把商业逻辑放到客户端(servlet,applet,等等)有严重的
负面效果,影响性能和可维护性,如session facade
模式所解释的。问题可以被使用session facade模式
纠正,需要把商业逻辑放到session bean中,session
bean的每个方法映射到一个特定的工作单元,或者
用例。这样做,客户端被从服务器端的对象模型屏蔽
起来,并且在一个事务和一次网络调用的round trip中
执行用例。
session facade模式自己是ejb开发的关键,不过也有
它自己的缺点。直接从客户端调用session facade会导
致客户端和服务器之间的依赖(在一个大型项目和复杂的
客户端代码中),因为对EJB的紧耦合,如Business
Delegate模式所讨论的。这些问题能被用 business delegate
解决,增加一个封装所有对ejb层的存取的对象层。business
delegate能帮助让客户端代码简单,使客户端和服务器之间的
依赖最小。
然后session facade模式和business delegate模式一起提供了
使客户端从服务器端的实现细节解耦并允许在一个网络调用
和一个事务中执行用例的格式下写商业逻辑的最好的实践。
和通常一样,有trade-off:
1.更慢的开发过程。因为用例逻辑(经常会变化的)在一个
session bean中运行,任何时候一个用例需要改变(就是,
增加一个参数到一个方法或返回一个额外的属性),实现
那个用例的session bean方法可能需要修改。改变一个
session bean的过程不是可以忽略不计的—-一个改变通常
需要编辑3个不同的文件(接口,bean class,deployment
descriptor)并且对ejb server的重发布和可能的重起服务器。
附加的,封装变化的session bean的在客户端的business
delegate将也需要修改。
2.大型项目中的劳动分工将更困难。依赖于一个项目中跨
开发者分配工作的策略,session facade是导致不同组或
开发者互相争斗的瓶颈,因为它将是随着项目进展时经常
变化的主题。
3.在一个大型公司中服务器资源经常被一个小组控制。
对一个有建立了的并且正在工作的发布的ejb集合的大型
公司,很难让其他项目的小组对已有的类施加影响和改变。
简单说,用session facade和business delegate开发会导致
长期的变化->发布->测试的round-trip,会成为大型项目的
瓶颈。问题的关键是商业逻辑放在一个session bean层,
几乎是重量级的开发。
综上所述:
使用Command模式来封装商业逻辑到轻量级的command
bean,使客户端从EJB解耦,在一个网络调用中执行,作为
EJB层的一个facade。
一个command bean只是一个有get,set和一个execute方法的
普通Java类,和最初的command模式(四人帮(gof),1995)描述
的一样。应用到EJB,Command模式为了达到和session facade
和business delegate相同的好处提供了一个轻量级解决方案:
一个隐藏ejb层的对象模型,在一个事物和一次网络调用中执
行一个用例,完成使客户端从ejb解耦的facade。command模式
通过提供本地交互的类达到这一点,不过实际上在一个远程
ejb服务器上执行,对客户端透明。
Command被用来封装应用程序中的单独的工作单元。比如
placeOrder,transferFunds,等等的用例,将有它的商业/工作流
逻辑封装在只为那个用例的特定的Command,如图1.7所示。
和一个command交互的客户端十分简单。一旦一个客户端得到
一个command(创建一个或从一个factory得到,取决于实现),
它只是简单的对command设置属性,直到command包含所有需要
执行用例的数据。这时客户端能调用command的execute方法,
然后简单的执行command上的get直到得到所有command和用例
的结果数据。
当客户端执行command,有趣的事情在幕后发生。不是本地执行,
command实际上传输到一个远程ejb服务器并在ejb服务器的JVM
中执行。然而,所有的在执行用例的过程中被command调用的
ejb发生在ejb服务器上,一个用例能在一个事物中执行。这个
行为的实现机制晚些时候将在这个模式的讨论中讲解。
使用transfunds例子,一个客户端将设置用来取钱,存钱,传输
量的账号的ID。调用transfunds command的execute后,客户端
将得到最后账户的平衡,如1.8图所示。
可能Command模式最完善的实现之一是IBM的Command框架,和websphere
一同出现,是IBM为电子商务的模式的一部分。有很多实现ejb command
模式的方法,不过他们都有3个要素:
1.Command bean。一个有get,set和一个包含需要执行一个用例的商业逻辑
的execute方法的简单的java bean。command bean是应用程序开发者需要
写的command模式的唯一部分,下面所解释的其他组件是可以跨工程复用的。
2.客户端路由逻辑。通常负责执行命令(command)并把它发送到远程ejb
服务器的一个类的框架。这个路由逻辑通常对客户端不可见,通过调用
command的execute方法来触发。路由逻辑/框架是一个普通的能被跨工程
复用的类的集合。
3.远程Command server。Command server是简单的接受命令(commands)
并执行它们的服务。应用到ejb,command server类是一个接受命令(command)
作为参数并本地执行之的stateless session bean。Command server也是
普通的(generic)并且完全跨项目可复用。
客户端和这3个组件之间的交互如图1.9所示。在这个例子中,客户端调用
路由逻辑组件上的executeCommand方法。在IBM command框架中,客户端
只需要调用command自己的execute,因为方法调用将实际上被command的
超类接收到,它是路由逻辑框架的一部分。
在幕后,CommandExecutor代理了对一个ejb command目标(因为它是路由
逻辑的一部分,所以没有在图1.9中表示出来)的调用,它被编码成知道ejb
并且知道怎样发送命令(command)到command server stateless session
bean。通过接受命令(command),command server简单的调用command的
execute方法,command然后继续它的商业逻辑。
Command模式的好处如下:
1.因为轻量级的开发/分发过程,方便了RAD。
把一个用例写成Command bean比写成一个session
bean方法相对更容易和快速去分发和测试。经常
的变化能在一个普通java类上做,而不是一个
完全的EJB。
2.把商业逻辑从表示逻辑分离。Command通过封装
command内的商业逻辑来作为服务器上对象模型
的一个facade,只暴露一个简单的command接口
让客户端使用。这个分离让客户端和服务器分开
的演进。
3.强制用例在一个单独的round trip中执行。
因为command实际上在EJB服务器上执行,只需要
一次网络调用(和一个事务)来完成一个复杂的
用例。
4.使客户端从ejb解耦。客户端是完全的从服务器的
实现细节解耦的–所有它们能看见的只是command bean,
command bean看上去象是本地类。
5.命令(command)可以本地执行或产生哑(dummy)数据。
空的或虚的命令(command)能在项目开始前被创建,
允许表示层开发者去对于商业逻辑和ejb小组相对独立的
写,编译,和测试他们的代码。
很多方面command模式听起来像个终极的解决方案,综合了
session facade和business delegate的好处,和一个
轻量级的基础。然而,好处和通常一样,被重要的trade-off
所平衡:
1.非常粗粒度的事务控制。因为command只是普通java bean,
没有自动的标记一个command去在一个特定的事务设置或
isolation level下运行的方法,而你用session bean方法可以。
Command只能在执行它们的Command server的事务设置下运行。
这个的结果是用不同的jndi名字和事务设置(在deployment
descriptor中配置)来分发多个command server session bean。
路由逻辑组件需要被配置成发送特定的命令(command)到command
server。就是说,一种方法想发送只读command到没有事务的
session bean,然而更新命令能在command server下用tx_required
和可序列化的isolation level运行。
2.command是无状态的。command对象不能存储任何状态到执行它的
session bean中。用command模式在ejb层存储状态是不可能的。
3.笨拙的错误处理。因为command框架是通用的(generic),从
command只有CommandException能被抛出。这意味着,应用程序
异常,如NoMoneyAccountException,需要被捕获并用CommandException
封装。然后客户端需要为了特定的异常透视到command对象里面。因为
异常不是显式的宣称的,客户端失去了编译期检查异常处理的好处。
4.command在大型项目中会变得无法管理。用成千的command,
大型项目会爆炸的,很多command有重复的商业逻辑的部分,
特别当不同的项目小组使用同样的后端领域模型。这使得
维护商业逻辑层比起在session bean方法中实现用例的session
facade(很好的分组到数目很小的session bean中)困难得多。
这种类的激增将是大型项目的严重问题。
5.Command Server ejb-jar紧密的耦合到command bean和
其它ejb。因为command bean在command server的环境下
执行,为了使command bean反序列化和执行,command bean
类需要和command server session bean一起分发(在相同的
ejb-jar或EAR中)。这意味着只要command bean变化了,command
server session bean EAR或ejb-jar将需要重新分发(因此command
server classloader能读到所有包含的command的新版本),为了
测试变化,或完全重起(如果你的应用服务器不支持热分发)。
还有,command bean需要看见任何在它们的商业逻辑中使用到的
home,remote,local home,或local interface。这需要或者当
ejb被任何它们的command bean存取时command server分发到
相同的EAR,或者存取ejb的interface和command server的ejb-jar
打包到一起。
command模式和session facade模式一起提供了两个重要的好处:
他们作为一个facade和它们在一个网络round trip中执行。另一个
command模式比session facade模式好的主要优点是把客户端从
ejb解耦了,用business delegate和session facade一起也可以达
到。因此,开发者怎样从中选择呢?把command看作是更便宜(cheaper)
的session bean会有所帮助。它们是轻量级的,更快的先导开发过程,
以后来的差的可维护性作为代价。
相关模式
command(四人帮(gof),1995)
Data Transfer HashMap
持续集成 |
Martin Fowler & Matthew Foemmel著 透明 译 |
| 英文原文版权由Martin Fowler拥有Original text is copyrighted by Martin Fowler 原文链接:http://martinfowler.com/articles/continuousIntegration.html Martin Fowler Chief Scientist, ThoughtWorks 译者语:2002年1月23日,我们很荣幸的在UMLCHINA组织的网上交流中聆听了Martin Fowler先生的教诲。在交流中,Martin Fowler向所有中国软件开发者推荐了这篇文章:Continuous Integration(《持续集成》)。初读之下,我便感觉到了它的分量,AgileChina的林星也称赞:”其中的思想非常的好,大师就是大师。”然后,用了一周的时间,我终于把这篇文章翻译出来,以飨读者。 由于这是Fowler先生送给全体中国软件开发者的礼物,所以我绝对不敢独占。任何人都可以在任何地方随意转载本文,但是在转载时请保持本文完整性–包括标题、版权声明、原文链接、译者语……总之,请不要在转载的时候做任何改动或增删。另外,如果能在转载的时候顺手给我一个mail,我会更加高兴。 下面,请开始欣赏这篇精彩的文章。 在任何软件开发过程中都有一个重要的部分:得到可靠的软件创建(build)版本。尽管知道创建的重要性,但是我们仍然会经常因为创建失败而惊讶不已。在这篇文章里,我们将讨论Matt(Matthew Foemmel)在ThoughtWorks的一个重要项目中实施的过程,这个过程在我们的公司里日益受到重视。它强调完全自动化的、可重复的创建过程,其中包括每天运行多次的自动化测试。它让开发者可以每天进行系统集成,从而减少了集成中的问题。 ThoughtWorks公司已经开放了CruiseControl软件的源代码,这是一个自动化持续集成的工具。此外,我们还提供CruiseControl、Ant和持续集成方面的顾问服务。如果需要更多的信息,请与Josh Mackenzie(jmackenz@ThoughtWorks.com)联系。 本文有以下主要内容: 持续集成的优点 集成越频繁,效果越好 一次成功的创建是什么样的? 单一代码源 自动化创建脚本 自测试的代码 主创建 代码归还 总结 在软件开发的领域里有各种各样的”最佳实践”,它们经常被人们谈起,但是似乎很少有真正得到实现的。这些实践最基本、最有价值的就是:都有一个完全自动化的创建、测试过程,让开发团队可以每天多次创建他们的软件。”日创建”也是人们经常讨论的一个观点,McConnell在他的《快速软件开发》中将日创建作为一个最佳实践来推荐,同时日创建也是微软很出名的一项开发方法。但是,我们更支持XP社群的观点:日创建只是最低要求。一个完全自动化的过程让你可以每天完成多次创建,这是可以做到的,也是完全值得的。 在这里,我们使用了”持续集成(Continuous Integration)”这个术语,这个术语来自于XP(极限编程)的一个实践。但是我们认为:这个实践早就存在,并且很多并没有考虑XP的人也在使用着它。只不过我们一直用XP作为软件开发过程的标准,XP也对我们的术语和实践产生了深远的影响。尽管如此,你还是可以只使用持续集成,而不必使用XP的任何其他部分–实际上,我们认为:对于任何切实可行的软件开发活动,持续集成都是很基本的组成部分。 实现自动化日创建需要做以下几部分的工作: 将所有的源代码保存在单一的地点,让所有人都能从这里获取最新的源代码(以及以前的版本)。 使创建过程完全自动化,让任何人都可以只输入一条命令就完成系统的创建。 使测试完全自动化,让任何人都可以只输入一条命令就运行一套完整的系统测试。 确保所有人都可以得到最新、最好的可执行文件。 所有这些都必须得到制度的保证。我们发现,向一个项目中引入这些制度需要耗费相当大的精力。但是,我们也发现,一旦制度建立起来,保持它的正常运转就不需要花多少力气了。 持续集成的优点 描述持续集成最大的难点在于:它从根本上改变了整个开发模式。如果没有在持续集成的实践环境中工作过,你很难理解它的开发模式。实际上,在单独工作的时候,绝大多数人都能感觉到这种气氛–因为他们只需要与自己的系统相集成。对于许多人来说,”团队开发”这个词总让他们想起软件工程领域中的一些难题。持续集成减少了这些难题的数量,代之以一定的制度。 持续集成最基本的优点就是:它完全避免了开发者们的”除虫会议”–以前开发者们经常需要开这样的会,因为某个人在工作的时候踩进了别人的领域、影响了别人的代码,而被影响的人还不知道发生了什么,于是bug就出现了。这种bug是最难查的,因为问题不是出在某一个人的领域里,而是出在两个人的交流上面。随着时间的推移,问题会逐渐恶化。通常,在集成阶段出现的bug早在几周甚至几个月之前就已经存在了。结果,开发者需要在集成阶段耗费大量的时间和精力来寻找这些bug的根源。 如果使用持续集成,这样的bug绝大多数都可以在引入的同一天就被发现。而且,由于一天之中发生变动的部分并不多,所以可以很快找到出错的位置。如果找不到bug究竟在哪里,你也可以不把这些讨厌的代码集成到产品中去。所以,即使在最坏的情况下,你也只是不添加引起bug的特性而已。(当然,可能你对新特性的要求胜过了对bug的憎恨,不过至少你可以多一种选择。) 到现在为止,持续集成还不能保证你抓到所有集成时出现的bug。持续集成的排错能力取决于测试技术,众所周知,测试无法证明已经找到了所有的错误。关键是在于:持续集成可以及时抓到足够多的bug,这就已经值回它的开销了。 所以,持续集成可以减少集成阶段”捉虫”消耗的时间,从而最终提高生产力。尽管现在还不知道是否有人对这种方法进行过科学研究,但是作为一种实践性的方法,很明显它是相当有效的。持续集成可以大幅减少耗费在”集成地狱”中的时间,实际上,它可以把地狱变成小菜一碟。 集成越频繁,效果越好 持续集成有一个与直觉相悖的基本要点:经常性的集成比很少集成要好。对于持续集成的实践者来说,这是很自然的;但是对于从未实践过持续集成的人来说,这是与直观印象相矛盾的。 如果你的集成不是经常进行的(少于每天一次),那么集成就是一件痛苦的事情,会耗费你大量的时间与精力。我们经常听见有人说:”在一个大型的项目中,不能应用日创建”,实际上这是一种十分愚蠢的观点。 不过,还是有很多项目实践着持续集成。在一个五十人的团队、二十万行代码的项目中,我们每天要集成二十多次。微软在上千万行代码的项目中仍然坚持日创建。 持续集成之所以可行,原因在于集成的工作量是与两次集成间隔时间的平方成正比的。尽管我们还没有具体的衡量数据,但是可以大概估计出来:每周集成一次所需的工作量绝对不是每天集成的5倍,而是大约25倍。所以,如果集成让你感到痛苦,也许就说明你应该更频繁地进行集成。如果方法正确,更频繁的集成应该能减少你的痛苦,让你节约大量时间。 持续集成的关键是自动化。绝大多数的集成都可以而且应该自动完成。读取源代码、编译、连接、测试,这些都可以自动完成。最后,你应该得到一条简单的信息,告诉你这次创建是否成功:”yes”或”no”。如果成功,本次集成到此为止;如果失败,你应该可以很简单地撤消最后一次的修改,回到前一次成功的创建。在整个创建过程中,完全不需要你动脑子。 如果有了这样一套自动化过程,你随便想多频繁进行创建都可以。唯一的局限性就是创建过程本身也会消耗一定的时间。(译注:不过与捉虫所需的时间比起来,这点时间是微不足道的。) 一次成功的创建是什么样的? 有一件重要的事需要确定:怎样的创建才算是成功的?看上去很简单,但是如此简单的事情有时却会变得一团糟,这是值得注意的。有一次,Martin Fowler去检查一个项目。他问这个项目是否执行日创建,得到了肯定的回答。幸亏Ron Jeffries也在场,他又提了一个问题:”你们如何处理创建错误?”回答是:”我们给相关的人发一个e-mail。”实际上,这个项目已经好几个月没有得到成功的创建了。这不是日创建,这只是日创建的尝试。 对于下列”成功创建”的标准,我们还是相当自信的: 所有最新的源代码都被配置管理系统验证合格 所有文件都通过重新编译 得到的目标文件(在我们这里就是Java的class文件)都通过连接,得到可执行文件 系统开始运行,针对系统的测试套件(在我们这里大概有150个测试类)开始运行 如果所有的步骤都没有错误、没有人为干涉,所有的测试也都通过了,我们就得到了一个成功的创建 绝大多数人都认为”编译+连接=创建”。至少我们认为:创建还应该包括启动应用程序、针对应用程序运行简单测试(McConnell称之为”冒烟测试”:打开开关让软件运行,看它是否会”冒烟”)。运行更详尽的测试集可以大大提高持续集成的价值,所以我们会首选更详尽的测试。 单一代码源 为了实现每日集成,任何开发者都需要能够很容易地获取全部最新的源代码。以前,如果要做一次集成,我们就必须跑遍整个开发中心,询问每一个程序员有没有新的代码,然后把这些新代码拷贝过来,再找到合适的插入位置……没有什么比这更糟糕的了。 办法很简单。任何人都应该可以带一台干净的机器过来,连上局域网,然后用一条命令就得到所有的源文件,马上开始系统的创建。 最简单的解决方案就是:用一套配置管理(源代码控制)系统作为所有代码的来源。配置管理系统通常都设计有网络功能,并且带有让开发者轻松获取源代码的工具。而且,它们还提供版本管理工具,这样你可以很轻松地找到文件以前的版本。成本就更不成问题了,CVS就是一套出色的开放源代码的配置管理工具。 所有的源文件都应该保存在配置管理系统中。我说的这个”所有”常常比人们想到的还要多,它还包括创建脚本、属性文件、数据库调度DLL、安装脚本、以及在一台干净的机器上开始创建所需的其他一切东西。经常都能看到这样的情况:代码得到了控制,但是其他一些重要的文件却找不到了。 尽量确保所有的东西都保存在配置管理系统的同一棵代码源树中。有时候为了得到不同的组件,人们会使用配置管理系统中不同的项目。这带来的麻烦就是:人们不得不记住哪个组件的哪个版本使用了其他组件的哪些版本。在某些情况下,你必须将代码源分开,但是这种情况出现的几率比你想象的要小得多。你可以在从一棵代码源树创建多个组件,上面那些问题可以通过创建脚本来解决,而不必改变存储结构。 自动化创建脚本 如果你编写的是一个小程序,只有十几个文件,那么应用程序的创建可能只是一行命令的事:javac *.java。更大的项目就需要更多的创建工作:你可能把文件放在许多目录里面,需要确保得到的目标代码都在适当的位置;除了编译,可能还有连接的步骤;你可能还从别的文件中生成了代码,在编译之前需要先生成;测试也需要自动运行。 大规模的创建经常会耗费一些时间,如果只做了一点小小的改动,当然你不会希望重新做所有这些步骤。所以好的创建工具会自动分析需要改变的部分,常见的方法就是检查源文件和目标文件的修改日期,只有当源文件的修改日期迟于目标文件时,才会重新编译。于是,文件之间的依赖就需要一点技巧了:如果一个目标文件发生了变化,那么只有那些依赖它的目标文件才会重新编译。编译器可能会处理这类事情,也可能不会。 取决于自己的需要,你可以选择不同的创建类型:你创建的系统可以有测试代码,也可以没有,甚至还可以选择不同的测试集;一些组件可以单独创建。创建脚本应该让你可以根据不同的情况选择不同的创建目标。 你输入一行简单的命令之后,帮你挑起这副重担常常是脚本。你使用的可能是shell脚本,也可能是更复杂的脚本语言(例如Perl或Python)。但是很快你就会发现一个专门设计的创建环境是很有用的,例如Unix下的make工具。 在我们的Java开发中,我们很快就发现需要一个更复杂的解决方案。Matt用了相当多的时间开发了一个用于企业级Java开发的创建工具,叫做Jinx。但是,最近我们已经转而使用开放源代码的创建工具Ant(http://jakarta.apache.org/ant/index.html)。Ant的设计与Jinx非常相似,也支持Java文件编译和Jar封装。同时,编写Ant的扩展也很容易,这让我们可以在创建过程中完成更多的任务。 许多人都使用IDE,绝大多数的IDE中都包含了创建管理的功能。但是,这些文件都依赖于特定的IDE,而且经常比较脆弱,而且还需要在IDE中才能工作。IDE的用户可以建立自己的项目文件,并且在自己的单独开发中使用它们。但是我们的主创建过程用Ant建立,并且在一台使用Ant的服务器上运行。 自测试的代码 只让程序通过编译还是远远不够的。尽管强类型语言的编译器可以指出许多问题,但是即使成功通过了编译,程序中仍然可能留下很多错误。为了帮助跟踪这些错误,我们非常强调自动化测试–这也是XP提倡的另一个实践。 XP将测试分为两类:单元测试和容纳测试(也叫功能测试)。单元测试是由开发者自己编写的,通常只测试一个类或一小组类。容纳测试通常是由客户或外部的测试组在开发者的帮助下编写的,对整个系统进行端到端的测试。这两种测试我们都会用到,并且尽量提高测试的自动化程度。 作为创建的一部分,我们需要运行一组被称为”BVT”(Build Verification Tests,创建确认测试)的测试。BVT中所有的测试都必须通过,然后我们才能宣布得到了一个成功的创建。所有XP风格的单元测试都属于BVT。由于本文是关于创建过程的,所以我们所说的”测试”基本上都是指BVT。请记住,除了BVT之外,还有一条测试线存在(译注:指功能测试),所以不要把BVT和整体测试、QA等混为一谈。实际上,我们的QA小组根本不会看到没有通过BVT的代码,因为他们只对成功的创建进行测试。 有一条基本的原则:在编写代码的同时,开发者也应该编写相应的测试。完成任务之后,他们不但要归还(check in)产品代码,而且还要归还这些代码的测试。这也跟XP的”测试第一”的编程风格很相似:在编写完相应的测试、并看到测试失败之前,你不应该编写任何代码。所以,如果想给系统添加新特性,你首先应该编写一个测试。只有当新的特性已经实现了以后,这个测试才可能通过。然后,你的工作就是让这个测试能够通过。 我们用Java编写这些测试,与开发使用同样的语言,所以编写测试与编写代码没有太大的区别。我们使用JUnit(http://www.junit.org/)来作为组织、编写测试的框架。JUnit是一个简单的框架,让我们可以快速编写测试、将测试组织为套件、并以交互或批处理的模式来运行测试套件。(JUnit是xUnit家族的Java版本–xUnit包括了几乎所有语言的测试框架。) 在编写软件的过程中,在每一次的编译之后,开发者通常都会运行一部分单元测试。这实际上提高了开发者的工作效率,因为这些单元测试可以帮助你发现代码中的逻辑错误。然后,你就没必要去调试查错,只需要注意最后一次运行测试之后修改的代码就行了。这个修改的范围应该很小,所以寻找bug也就容易多了。 并非所有的人都严格遵循XP”测试第一”的风格,但是在第一时间编写测试的好处是显而易见的。它们不但让每个人的工作效率更高,而且由这些测试构成的BVT更能捕捉到系统中的错误。因为BVT每天要运行好几次,所以BVT检查出的任何问题都是比较容易改正的,原因很简单:我们只做了相当小范围的修改,所以我们可以在这个范围内寻找bug。在修改过的一小块代码中排错当然比跟踪整个系统来排错要有效多了。 当然,你不能指望测试帮你找到所有的问题。就象人们常说的:测试不能证明系统中不存在错误。但是,尽善尽美不是我们唯一的要求。不够完美的测试只要经常运行,也比永远写不出来的”完美测试”要好得多。 另一个相关的问题就是:开发者们为自己的代码编写测试。我们经常听人说:开发者不应该测试自己的代码,因为他们很容易忽视自己工作中的错误。尽管这也是事实,但是自测试过程需要快速将测试转入代码基础中。这种快速转换的价值超过独立测试者的价值。所以,我们还是用开发者自己编写的测试来构造BVT,但是仍然有独立编写的容纳测试。 自测试另一个很重要的部分就是它通过反馈–XP的一项核心价值–来提高测试的质量。这里的反馈来自于从BVT中逃脱的bug。自测试的规则是:除非你在BVT中加入了相应的测试,否则就不能修正任何错误。这样,每当要修正某个错误的时候,你都必须添加相应的测试,以确保BVT不会再把错误放过去。而且,这个测试应该引导你去考虑更多的测试、编写更多的测试来加强BVT。 主创建 创建过程的自动化对于单个开发者来说很有意义,但是它真正发光的,还是在整个系统的主创建(master build)的生成。我们发现,主创建过程能让整个团队走到一起来,让他们及早发现集成中的问题。 第一步是要选择运行主创建的机器。我们选择了一台叫做”投石车”的计算机(我们经常玩”帝国时代”J),这是一台装有四个CPU的服务器,非常适合专门用来做创建。(由于完整的创建需要相当长的时间,所以这种马力是必须的。) 创建进程是在一个随时保持运行的Java类中进行的。如果没有创建任务,创建进程就一直循环等待,每过几分钟去检查一下代码仓库。如果在最后的创建之后没有人归还任何代码,进程就继续等待。如果代码仓库中有了新的代码,就开始创建。 创建的第一阶段是完全提取仓库中的代码。Starteam已经为我们提供了相当好的Java API,所以切入代码仓库也很容易。守护进程(daemon)会观察五分钟以前的仓库,看最近五分钟里面有没有人归还了代码。如果有,守护进程就会考虑等五分钟再提取代码(以免在别人归还代码的过程中提取)。 守护进程将全部代码提取到投石机的一个目录中。提取完成之后,守护进程就会在这个目录里调用Ant脚本。然后,Ant会接管整个创建过程,对所有源代码做一次完整的创建。Ant脚本会负责整个编译过程,并把得到的class文件放进六个jar包里,发布到EJB服务器上。 当Ant完成了编译和发布的工作之后,创建守护进程就会在EJB服务器上开始运行新的jar,同时开始运行BVT测试套件。如果所有的测试都能正常运行通过,我们就得到了一个成功的创建。然后创建守护进程就会回到Starteam,将所有提取出的源代码标记上创建号。然后,守护进程会观察创建过程中是否还有人归还了代码。如果有,就再开始一次创建;如果没有,守护进程就回到它的循环中,等待下一次的归还。 创建结束之后,创建守护进程会给所有向最新一次创建归还了代码的开发者发一个e-mail,汇报创建的情况。如果把创建留在代码归还之后去做,而又不用e-mail向开发者通报创建的情况,我们通常认为这是不好的组织形式。 守护进程将所有的步骤都写在XML格式的日志文件里面。投石车上会运行一个servlet,允许任何人通过它检查日志,以观察创建的状态。(见图1) 屏幕上会显示出创建是否正在运行、开始运行的时间。在左边有所有创建的历史记录,成功的、失败的都记录在案。点击其中的某一条记录,就会显示出这次创建的详细信息:编译是否通过、测试的结果、发生了哪些变化…… 我们发现很多开发者都经常看看这个页面,因为它让他们看到项目发展的方向,看到随着人们不断归还代码而发生的变化。有时我们也会在这个页面上放一些其他的项目新闻,但是需要把握好尺度。 要让开发者能在自己的本地机器上模拟主创建过程,这是很重要的。这样,如果集成错误出现了,开发者可以在自己的机器上研究、调试,而不必真的执行主创建过程。而且,开发者也可以在归还代码之前先在本地执行创建,从而降低了主创建失败的可能性。 这里有一个比较重要的问题:主创建应该是干净的创建(完全从源代码开始)还是增量创建?增量创建会快得多,但是也增大了引入错误的风险,因为有些部分是没有编译的。而且我们还有无法重新创建的风险。我们的创建速度相当快(20万行代码约15分钟),所以我们乐于每次都做干净的创建。但是,有些团队喜欢在大多数时候做增量创建,但是当那些奇怪的问题突然出现时,也经常性地做干净的创建(至少每天一次)。 ![]() 图1:运行在投石车上的servlet 代码归还(Check in) 使用自动化创建就意味着开发者应该遵循某种节奏来开发软件,最重要的就是他们应该经常集成。我们曾经见过一些组织,他们也做日创建,但是其中的开发者却不经常归还代码。如果开发者几周才归还一次代码,那么日创建又有什么意义呢?我们遵循的原则是:每个开发者至少每天要归还一次代码。 在开始新的任务之前,开发者应该首先与配置管理系统同步。也就是说,他们应该首先更新本地机器上的源代码。在旧的代码基础上编写代码,这只会带来麻烦和混乱。 然后,开发者要随时保持文件的更新。开发者可以在一段任务完成之后将代码集成到整个系统中,也可以在任务的中途集成,但是在集成的时候必须保证所有的测试都能通过。 集成的第一步是要再次使开发者的本地文件与代码仓库同步。代码仓库中所有新近有改动的文件都要拷贝到开发者的工作目录中来,当文件发生冲突时,配置管理系统会向开发者提出警告。然后,开发者需要对同步后的工作集进行创建,对这些文件运行BVT,并得到正确的结果。 现在,开发者可以把新的文件提交到代码仓库中。提交完成之后,开发者就需要等待主创建。如果主创建成功,那么这次归还也是成功的。如果主创建失败了,开发者可以在本地修改。如果修改很简单,就可以直接提交;如果修改比较复杂,开发者就需要放弃这次修改,重新同步自己的工作目录,然后继续在本地开发、调试,然后再次提交。 某些系统强制要求归还进程逐个进行。在这种情况下,系统中会有一个创建令牌,同一时间只有一个开发者能拿到令牌。开发者获取创建令牌,再次同步文件,提交修改,然后释放令牌。这就确保创建过程中,最多只能有一个开发者在更新代码仓库。不过我们发现,即使没有创建令牌,我们也很少遇到麻烦,所以我们也不用这种方法。经常会有多个人同时向同一个主创建提交代码的情况,但是这很少造成创建失败,而且这样的错误也很容易修复。 同时,我们还让开发者自己来决定归还过程中的小心程度。这反映出开发者对集成错误出现几率的评估。如果她觉得很有可能出现集成错误,那么她就会在归还之前先做一次本地创建;如果她觉得根本不可能出现集成错误,那么她可以直接归还。如果犯了错误,在主创建运行时她立刻就会发现,然后她就必须放弃自己的修改,找到出错的地方。如果错误很容易发现、很容易修补,那么这种错误也是可以接受的。 总结 发展一个制度严密的自动化创建过程对于项目的控制是很重要的。许多软件先贤都这样说,但是我们发现,这样的过程在软件开发领域中仍然罕见。 关键是要让所有的事情都完全自动化,并且要经常进行集成,这样才能尽快发现错误。然后,人们可以随时修改需要修改的东西,因为他们知道:如果他们做的修改引起了集成错误,那也是很容易发现和修补的。一旦获得了这些利益,你会发现自己再也无法放下它们。 |
作者:紫龙 发表时间:2001/11/16 10:18pm | |
|
作者:eclipse 发表时间:2002/09/05 08:17am | |
|
