经验二:应用注释(annotation)生成代码 项目初期,开发团队采用XDoclet1来生成部署描述器(deployment descriptors),值对象(value objects),查询,会话facades,Struts表和动作,值验证,以及本地和远程EJB接口等。由于项目的短时间开发周期(short time frame)计划,需要应用一些额外技术帮助开发团队在短时间内和保证减少bug的前提下尽可能多产,代码生成也就成为这个项目中的一个关键策略。也因为它的关键性,最初的XDoclets模板后来被修改生成“专家代码”。
起初的10个月里,XDoclet方式在项目进行中很适用,帮助开发团队达到了所需的开发能力,但没过多久,随着代码的不断增多,生成代码的速度却越来越慢,以至于生产率惨遭严重影响。于是,开发团队决定采用另一种代码生成策略,这就是Java 5 注释 (annotations)。
注释策略可以解决XDoclet的以下几项缺点:
- 注释允许像部署描述器这样渐增的代码生成文件:XDoclet中,假使一个类被修改,XDoclet需要再次读取所有类才能生成新的部署描述器。但使用注释的话,可以仅仅重新生成部署描述器中由于类被修改而受影响的部分。鉴于大部分时候,开发人员只修改很少一部分类,注释策略的采用就帮助省去很多时间。
- Xdoclet在生成文件的过程中需要访问子组件的源代码,这增加了连接和构建的复杂度。所有开发人员需要同样的权限访问所有源代码,而非如最初计划那样仅访问编译后的jar包。但采用注释的话,就可以避免这个问题。
- 注释可以在运行时(runtime)被处理,除了生成代码,这个特点可以带来更多好处。
团队为EJB 3.0 注释和其他一些自定义注释开发了处理器,使其最后生成的代码和采用XDoclet时所生成代码的相似。这些生成的代码在EJB 2.1下也同样兼容。
注释处理器使用Velocity模板。从XDoclet转移到自己编写使用的模板中,最大的挑战是构建和XDoclet提供的同样容易理解的一组模板和帮助类。
采用注释以后,代码生成时间缩短很多。使用XDoclet,如果其中一个类有所修改,需要1分50秒的时间从400个类中重新生成代码,但是使用APT以后,同样的操作仅需10秒钟。
经验三:规则引擎如何简化业务逻辑 根据政府立法,一些业务逻辑一直在修改中,有些按城市各异,系统某些部分不得不处理这些特殊的业务逻辑。比方说,某诊所想要提供x光透视服务,这等价于几项服务规则,拥有x光透视设备,有授权资格的放射科医生等。由于这些规则经常在变,所以最好把它们留在主代码库外头,这样即使规则被修改也不用去修改代码。
在SIGA-Saude系统中,因为引入了
Drools 规则引擎而使这个难题迎刃而解。Drools实现了JSR-94-Java 规则引擎应用程序接口。规则由SIGA-Saude系统中一个称作Decision的控件来处理。该控件工作于一组规则组和一个工作记忆区(working memory)。规则组工作于一组消息,每条规则都相应有一条由实现了br.com.vidatis.common.decision.message.RuleMessage接口的类描述的消息。当一条规则或是规则组的子集被满足的时候,对应的消息会从队列中移除,这样做可以追踪规则的处理。如果最后在队列中没有任何消息剩下,表明规则组中的所有的规则都被满足。任何一条规则都可以触发许多不同的动作,也可以触发新的规则组,规则在XML文件中描述,然后交由Drools引擎处理。
XML文件范例:
<rule name="if_clinic_code_then_checksum_digit_valid">
<parameter identifier="clinic">
<java:class>br.atech.smssp.domain.clinic.vo.ClinicVO</java:class>
</parameter>
<java:condition>clinic.getCode() != null</java:condition>
<java:condition>!clinic.getCode().equals("")</java:condition>
<java:consequence>
//clinic code should be mandatory and valid
if(br.com.vidatis.common.decision.rule.CodeRule.isValidCone(clinic.getCode())){
ruleMessage.markRuleAsValid("if_clinic_code_then_checksum_digit_valid");
}
</java:consequence>
</rule>
<rule name="if_maintainer_code_then_checksum_digit_valid">
<parameter identifier="clinic">
<java:class>br.atech.smssp.domain.clinic.vo.ClinicVO</java:class>
</parameter>
<java:condition>clinic.getMaintainerCode() != null</java:condition>
<java:condition>!clinic.getMaintainerCode().equals("")</java:condition>
<java:consequence>
//Maintainer code is optional, but should be valid if it is informed.
if(br.com.vidatis.common.decision.rule.CodeRule.isValidCode(clinic.getMaintainerCode())){
ruleMessage.markRuleAsValid("if_maintainer_code_then_checksum_digit_valid");
}
</java:consequence>
</rule>
<rule name="if_maintainer_code_then_checksum_digit_valid_OPTIONAL_EMPTY">
<parameter identifier="clinic">
<java:class>br.atech.smssp.domain.clinic.vo.ClinicVO</java:class>
</parameter>
<java:condition>clinic.getMaintainerCode() != null</java:condition>
<java:condition>clinic.getMaintainerCode().equals("")</java:condition>
<java:consequence>
//Maintainer code is optional and can be empty
ruleMessage.markRuleAsValid("if_maintainer_code_then_checksum_digit_valid");
</java:consequence>
</rule>
<rule name="if_maintainer_code_then_checksum_digit_valid_OPTIONAL_NULL">
<parameter identifier="clinic">
<java:class>br.atech.smssp.domain.clinic.vo.ClinicVO</java:class>
</parameter>
<java:condition>clinic.getMaintainerCode() == null</java:condition>
<java:consequence>
//Maintainer code is optional and can be NULL
ruleMessage.markRuleAsValid("if_maintainer_code_then_checksum_digit_valid");
</java:consequence>
</rule>
这个例子中,保存某诊所数据的时候,服务类会调用Drools规则引擎检查和认证所有的规则,数据只有符合所有的规则才能被储存。这些认证规则随着政府制定的规章制度的修改而变化。由于修改经常发生,所以把它们和应用程序代码划分开来非常重要,这样才能在维护和修改程序的时候不需要重启整个应用系统。
教训与启发 代码生成在这个项目中是成功的关键,它不但提高了开发人员的生产率,同时也保证了代码风格一致。在50个开发人员在各个独立团队中各自工作且共享基础组件的情况下,使他们保持代码一致性非常困难。起初采用XDoclet,然而好景不长,由于生成文件时间越来越长,对开发速度带来出乎意料的坏影响。之后,转而采用注释策略,才大大地减短了生成时间,真正体现了生成代码的优越性。
好的团队交流是必须的。特别是像这样的大项目,总会有些时候有些人发表不同的意见。让所有的人互相理解对方的观点实在很难。特别是在这样一群人中,交流真的是让人头疼的难题:
- 开发者 VS. 构架师——你不得不吃你自己的狗食。当构架师积极参与到开发工作中来的时候,他们可以提出更好的建议,提高生产率。同时团队的“开心指数”也相对得到提升,同样也影响到整体的开发能力。
- 需求分析者/客户 VS. 开发人员/构架师——这两组人交流的失败可以直接导致项目的推迟并让所有人灰心丧气。好的规格说明是必要的,但要这两组人理解双方的动机,仅靠一个好的用例说明来使他们融洽得像一个团队那样工作简直是不可能的。
- 出类拔萃的开发人员 VS. 其他出类拔萃的开发人员——这个项目中有个开发小组,拥有非常优秀的开发人员,他们中的每个人都有满脑子的好点子来开发软件。然而在短时开发周期中,这个组的组员每天都冒出一些无法验证可能性的改革性的主意,这个小组因此受到一些挫折。其实在像这样的项目中,重要的是坚持定义严谨的终极目标,并使每个人都能正确理解作出每个决定的理由。这个开发小组后来采取间断性的重构周(refactor),在这些重构周里,组员提出系统中存在的问题,讨论如何解决这些问题并在这段时间里对系统进行重构。整个星期都只作重构,没有任何新代码的开发,只用来修改原有代码使之更便于实现未来将冒出来的新点子而提高工作效率。
如果你的构架定义确切,有规则、有代码生成、套用设计模式、构件的话,就比较容易进行小范围或中等程度的软件重构,大范围重构往往很麻烦。然而,现实系统的部分构架不够确切,严格的规则更使得20%的应用程序那以实现。明确你究竟能够在加强构架标准这点上走多远至关重要。项目开发过程中,有时候就算一些解决方案显然不是最好的,开发人员也只是强行将它们向构架靠拢,而非重新探讨最佳方案。另一方面,系统中某些部分,规则要求并不严格,比如说用户界面,对于这些部分,开发人员则各取其爱,以至于造成系统这些部分质量较差、代码不易管理等预料未及的坏结果。
在庞大的应用程序中,面对繁杂的依赖性和代码,开发周期往往不得不延长。有时候,在某个界面上加上一个简单的域(field)就意味着系统的许多部分都随之需要修改,这很费时间。无论是开发人员还是客户都因此感到头痛,工具变得太过沉重以至于所有一切都比原本更花时间。把应用程序划分成许多构件,还是不足以彻底解决问题。尽管J2EE有很多优点,但同时代码也更加复杂,生产率还是受到影响,这些很难跟管理人员和客户解释清楚。
项目启动最初,很多人都说按照那样的短时开发周期,项目根本没有办法完成。在其他一些国家的公共医疗保健系统开发过程中,也都发生过太多太多令人汗颜的经验。所以,从这个项目中得到的最后一个启示是:当别人说某件事不可能的时候,千万不要让他们这样的泛泛之说影响了你去将这件事变得可能。
未来方向容器外测试 无法进行容器外测试让开发团队很困扰,无法在初期就进行容器外测试的原因是他们被迫从合法的代码开始工作。政府制定的规则和规章条例模块是一个巨大的EJB 1.0系统。结合代码生成技术来处理所有的实例bean和其他容器假象(container artifact),程序的新生部分变的越来越基于POJO。但将他们部署到容器中以后,甚至只是一个渐进变化也缓慢到开发人员无法接受的地步。
迁移到基于POJO的构架 目前的系统明显有大量的容器依赖和EJB样板代码(plumbing code)(尽管大部分都隐藏在生成的业务委托和会话外观中),但开发团队计划的迁移方向是完完全全基于POJO,包括业务规则,他们计划将所有的会话Bean重构成一个单一的会话bean interceptor,由这个单一的会话bean interceptor专门向服务层调用。构建这个单一的会话bean interceptor的目的在于提供像事务(transaction)和线程管理这样的中间件服务。他们原先设想在修改之后的构架上采用Spring,但这注定开销太过昂贵。鉴于当时拥有的会话bean都已经全部生成,相比较从EJB中完全引身而退来说,修改代码生成逻辑来贯彻会话bean interceptor明显简单得多。将这样的重构变得可能的另一个元素是现存的业务委托层可以将这些大幅修改从Web端屏蔽。
为什么他们没有一并摆脱掉EJB呢?理由是最后他们必须将应用程序展示给在医疗卫生部门的另外的团队,这些团队需要访问该系统,并正准备简单地让工作人员(从圣保罗市)接手开始投入使用可行的业务委托。由于业务委托隐藏了远程的各方面,他们知道合作方将不会工作于Java上,所以预期系统集成会非常顺利。
不同的事务类型又将如何处理呢?不同的事务界限有不同的方法,目标方法和事务类型间有一个映射,所以他们知道该执行哪个方法来处理相应的事务。顺便提一句,这里的会话Bean interceptor和事务策略同Floyd Marinescu在EJB设计模式一书中描述的极为相近。 :)
最终,他们实在是想摆脱掉实例Bean,也许甚至想直接只保存值对象和Hibernate。面对这样修改的潜在可能性最大花销是测试,他们必须彻彻底底地测试系统的每个方面,单一的单元测试无法覆盖全部。
AJAX简化Web用户界面 AJAX被公认是为非技术终端用户简化用户界面工作流程的有效工具。就如在调度用例中,自动补全(auto-completion)可以帮助输入医生姓名,疗程名称,专科名称,实在没有理由去调用弹出窗口来做查询,从长长的列单中去寻找这些名字。另外,在使用大而冗长的表单时,使用AJAX一步一步将表单域的值保存到HTTPSession中去无疑会更好,还能防止一些用户因不小心错关页面而遗失所输入的信息。
最近AJAX也被部署项目中,被用来浏览一个需要读取数据的巨大的流程树,帮助我们在缩短了数据录入时间的同时也给终端用户提供更漂亮的界面。下面是界面截图:

附件:
您所在的用户组无法下载或查看附件