写在前面的话
这是2006年,我参加华中科技大学硕士论文答辩写的一篇论文。但是,这篇论文写的太过Blog化,被导师否决了。我打算重写一篇学位论文,所以,这里把这篇过气的论文公布出来。
我是华中科技大学电子信息工程系电子信息工程硕士研究生。 大家可以给我写E-Mail来联系我。我主要使用的语言是Java。Java社区非常活跃,至少现在是如此,可以说是现在最先进的一门编程语言。当然,未来,一切还未注定!
动态面向对象语言,比如Smalltalk,Ruby,Python等这些语言,也非常有魅力。我相信,动态面向对象的机制,在未来应该得到发展和应用。Java1.3引入的动态代理,已经为我们展现了强大的威力。
Smalltalk仅仅只有几个关键词,就构成了一门强大的面向对象语言。动态面向对象语言,不需要类型声明,如果实际类型一致,直接调用好了!当然,也许这并不是静态面向对象编程语言的缺点,毕竟,编译器差错能够帮助我们减少错误。不过,如果能够在编写代码时,模拟运行,也许也能够在编写代码时为我们找出这些错误。
6月6日,我参加了Martin Fowler在上海交大的敏捷开发座谈会。会上,Martin Fowler做了关于Ruby的演讲。其中,将Ruby和DSL(特定领域语言)联系到了一起。他认为,Ruby简洁的语法,是DSL语言的理想表达工具。
前些天,我看到Intellj的作者的一篇文章,也提出了发展DSL的设想,他正在致力于将Intellj制作成开发DSL的IDE工具。
Martin Fowler认为目前xml形式的DSL表达方式过于复杂,应该使用普通英语直接表达。目前,我还是认为XML格式的DSL比较好。我们可以自己用Java等语言编写解释程序,解释自定义的xml形式的文本----DSL。XML格式的DSL的优点是便于验证。
总之,DSL目前尚未发展成熟,但是未来的前景还是非常乐观的。不过,现在跟进,可能还太早。
将来,AOP也会有很大的发展。AOP的混入机制,是AOP最强大的机制,未来如果有合适的场合,应该会成为程序员手中又一个杀手锏。
我想,未来可能会出现这样一种语言:她集中了静态面向对象编程语言和动态面向对象编程语言的机制于一身,还直接支持AOP这样的编程范式。
尽管ASpectJ已经扩展了Java。但是,由于并不是标准的Java,所以,大家使用起来仍然有困难。当然,现在Java不把AspcetJ引入Java标准,可能也因为现在AOP还没有成熟,众多的产品和理念还没有决出胜负。
未来这样的一种语言,也许是未来的Java,也可能是一种全新创造的语言,或者是一门动态面向对象编程语言的扩展。不过,没关系,还是Java的理念。到时候,Java社区的程序员也是可以毫不费力的转到新的社区的!
在本文的最后,有一节是讲“源代码就是设计”。这里讲到了,写源代码和写文章很类似。不过,本人虽然博学,但是对怎样写文章,这样一门学问,确实没什么研究。我希望,未来有这方面背景的程序员,能够将文章学引入到程序员的世界来,这也是一件造福人类的事情!
文学系的朋友们,你们论文的题目有了啊^-^
好了,废话不说了,请看在下得拙著吧!
2006-06-23 于上海家中
目 录
摘要…………………………………………………………………………………….I
ABSTRACT…………………………………………………………………………..II
1 什么是企业级应用 4
2 为什么我们需要开发健壮的企业级应用 6
3 什么是健壮的企业级应用 7
3.1 什么是健壮的企业级应用 7
3.2 企业级应用的一般结构 7
3.3 健壮的企业级应用的一般结构 8
4 怎样开发健壮的企业级应用 11
5 面向对象编程技术 12
5.1 依赖于抽象,而不要依赖于具体实现 12
5.2 使用委派而不是继承 13
5.3 “客户—服务器”关系中,应该是“瘦”客户类,“胖”服务器类 15
5.4 类存在的意义是提供的服务,而非保存的数据 15
5.5 单一功能的方法 17
5.6 单一职责的接口 18
5.7 用接口来隔离实现类 18
5.8 直接使用编程语言的概念进行设计 19
5.9 尽量使用模式来解决问题 19
6 面向方面编程技术 20
6.1 AOP的重要概念 22
6.2 实现AOP的主要技术策略 25
6.3 Spring AOP框架 27
6.4 如何更好的使用AOP 28
7 面向关注软件开发 29
8 敏捷开发方法的代表作—XP 30
8.1 XP的前提 30
8.2 为什么需要XP 31
9 融合XP的软件开发过程 32
9.1 获取需求 32
9.2 测试驱动开发和验收测试级重构 33
9.3 单元测试驱动开发和单元测试级重构 35
9.4 小结 36
10 使用Java开发企业级应用的参考架构 36
10.1 JavaEE 36
10.2 “经典”的JavaEE架构 36
10.3 Java开源软件 38
10.4 不用EJB的简单Java EE架构 38
10.5 使用“轻量级容器”的Java EE架构 42
11 总结 45
11.1 “源代码就是设计” 45
11.2 总结 45
致 谢 46
参 考 文 献 46
什么是企业级应用
企业级应用(Enterprise Applications),顾名思义,就是企业经营中需要使用的应用程序。又叫作企业信息系统(Enterprise
Information System)或者管理信息系统(Management Information Systems)。其主要任务是最大限度的利用现代计算机及网络通讯技术加强企业的信息管理,通过对企业拥有的人力、物力、财力、设备、技术等资源的调查了解,建立正确的数据,加工处理并编制成各种信息资料及时提供给管理人员,以便进行正确的决策,不断提高企业的管理水平和经济效益。可以说,它的涵盖面是非常广的,很难给它下一个确切的定义,但是我可以罗列一些个人的理解。[1]
先举几个例子。企业级应用包括工资单、患者记录、发货跟踪、成本分析、信誉评估、保险、供应链、记账、客户服务以及外币交易等。企业级应用不包括车辆加油、文字处理、电梯控制、化工厂控制器、电话交换机、操作系统、编译器以及电子游戏等。
企业级应用一般都涉及到持久化数据。数据必须持久化是因为程序的多次运行都需要用到它们—实际上,有些数据必须持久化若干年。在此期间,操作这些数据的程序往往会有很多变化。这些数据的生命周期往往比最初生成它们的那些硬件、操作系统和编译器还要长。在此期间,数据本身的结构一般也会被扩展,使得它在不影响已有信息的基础上,还能表示更多的新信息。即使是有根本性的变化发生,或公司安装了一套全新的软件,这些数据也必须被“迁移”到这些全新的应用程序上。
企业级应用一般都涉及到大量数据—一个中等规模的系统往往都包含1GB以上的数据,这些数据是以数百万条记录的方式存在的。巨大的数据量导致数据的管理成为系统的主要工作和挑战!早期的系统使用的是索引文件系统,如IBM的VSAM和ISAM。现代的系统往往采用数据库。数据库有层次数据库、网状数据库、关系数据库和对象数据库,还有对象—关系数据库。现最成熟,应用也最多的是关系数据库。数据库的设计和演化已使其本身成为新的技术领域。
数据库在企业级应用中处于重要的地位。选择性能优良的数据库和有效的使用数据库,是开发企业级应用的一项核心工作!
最近兴起的使用XML文件存储少量数据这一技术。实际上,XML文件格式,就是早期的层次数据库。它具有丰富的表达能力和简单的优点,但是数据库发展的历史已经表明,XML文件不可能取代现在的关系数据库和对象—关系数据库。
企业级应用一般还同时涉及到很多人同时访问数据。对于很多系统来说,人数可能在100人以下,但是对于一些基于Web的系统,人数则会呈指数级增长。要确保这些人都能够正确地访问数据,就一定会存在这样或那样的问题。即使人数没有这么多,要确保两个人在同时操作同一数据项时不出现错误,也是存在问题的。事务管理工具可以处理这个问题,但是最为程序员,我们仍然要正确地使用它们,这同样不容易做到。
企业级应用还涉及到大量操作数据的用户界面。有几百个用户界面是不足为奇的。用户使用频率的差异很大,他们也经常没什么技术背景。因此,为了不同的使用目的,数据需要很多种表现形式。
企业级应用也很少单独存在,通常需要与企业的合作伙伴的企业级应用集成。这些各式各样的系统是在不同时期,采用不同的技术开发的。甚至连通讯机制都不相同:基于Socket的系统、CORBA系统、COM系统、Java的RMI、EJB和消息系统,以及最新的Web
Service等。企业经常希望能够用一种同一的通信技术来集成所有的系统。然而,每次这样的集成工作都很难真正实现,留下来的就是一个个风格各异的集成环境。
即使是某个企业统一了集成技术,他们还是会遇到业务过程中的差异,以及数据库中数据概念的不一致性。不同部门、不同系统,甚至不同时期,都会对业务数据有不同的定义和理解。
随着企业级应用的发展和集成,整个企业的企业级应用程序就成了一个不同技术、不同数据混杂在一起组成的复杂系统。不断的修改和添加新功能,也使系统的Bug越来越多,修改和添加新功能变得越来越困难。
对于一些人来说,“企业级应用”这个词指的是大型系统。但是要注意,并不是所有的企业级应用都是大型的,尽管它们可能都在为企业提供巨大的价值。
为什么我们需要开发健壮的企业级应用
开发企业级应用程序并不是一件简单的事情,开发出一个能令客户满意的企业级应用程序更非易事。这需要考虑太多的事情、克服太多的难关—技术上、商业上、人际关系上的。
事实上,企业级应用的主要成本和困难并不在开发软件这个时期,而是在软件的维护阶段。企业级应用在其交付使用后,其维护阶段在软件生命周期或生存期中占较大比重,有的可达软件生命周期总成本(TCO)的50-70%。因为,企业的业务环境一直处于不断的变化之中,这就要求企业级应用也要能够适应企业的变化。而这些可能的变化,对于开发者来说,是很难预见的。如果开发的企业级应用,其架构没有很强的适应能力、不够健壮的话,那么在维护阶段修改软件,或者增加新功能将是极其困难的,甚至是不能做到,必须推倒重来!
业务需求的变化,才是企业级应用最大的风险和难点!而且,这种变化,基本上每个项目都会出现,这是企业级应用开发的“常态”。甚至,一般在软件开发的过程中,就会出现业务需求的变化。
现在,市面上有很多套装的企业级应用,如ERP,CRM,财务软件等。很多企业花了大价钱买了回来,但是应用下来,失败的案例很多。不仅造成了软件购置费用的浪费,更严重的是扰乱了企业正常的业务活动,造成了严重的损失。也使不少企业对信息化望而却步。
企业,作为市场经济的主体,其面临的内外部环境是在不断变化的,企业本身也会针对这种变化,经常性的调整其组织结构和业务流程。并不存在适用于所有企业的一套一成不变的组织结构和处理流程。
使用套装软件,就好比是“削足适履”,为了适应软件的需要,改变企业原来运转正常的组织结构和业务流程,实在是不智之举。企业级应用程序,是为企业服务的,应当服从企业的需要,而不是相反,企业成了软件的奴隶!
IBM的广告词“随需应变的软件”,就是企业级应用软件业者理想中的软件。可是,依靠IBM,就能够开发出“随需应变的软件”吗?当然不可能,一切只能够靠我们程序员自己才能做到!
什么是健壮的企业级应用
什么是健壮的企业级应用
“随需应变的软件”,就好像是塑胶泥,我们可以任意拿捏,变化出不同的形状。“随需应变的软件”,必然是健壮的软件,不论怎样折腾,都能够应对自如。
什么是“健壮的企业级应用”,对此我无法给出一个精确的定义,我只能够罗列一些我的理解。
“健壮的企业级应用”,其各个部分应该是低耦合、高内聚的。其内部的各个模块之间的关系最低,且可以互相替换,从而可以方便地拆卸、替换和修改各个模块。
其核心思想,就是“接口”。各个部分之间通过接口,并且只通过接口互相衔接,一起合作。只要接口相同,那么这些模块就可以互相替换。对于其他模块来说,其合作部分的具体实现是不重要的。
这样,我们在需要“随需应变”的改变软件时,只要简单的提供在原有系统上接插上不同的实现模块即可。
这实际上,就是面向对象(OO)思想的一种体现。更深入的说,就是“抽象”的思想。把具体的源代码通过接口屏蔽、抽象起来。这样,只要接口不变,那么无论源代码怎样变化,都不会影响整个软件的正常运行。
当然,不仅企业级应用需要“健壮”,任何软件都应该是健壮的。但是,在企业级应用程序的开发和维护过程中,由于其需求的多变性,就更需要是“健壮”的。
企业级应用的一般结构
现在,让我们再来看一看企业级应用的结构。企业级应用在结构上,一般可以分为三大模块:表现模块,业务模块,领域模块。这里,我将它们称作“模块”,而不是“层”。很多人喜欢划分“层次”,但我觉得划分“模块”更合适。因为,“层”有上下之分,只能是上层调用下层;而“模块”就没有上下之分,可以根据实际情况任意调用。这里,我不想分清什么是上层,什么是下层,用模块来表示应该更加合适。
一、表现模块
表现模块,又叫作客户端。用于向客户提供使用软件系统的途径。一般有图形用户界面GUI,命令行界面,文本文件等等。这个模块,仅仅是用来接收用户请求,再将这个请求委派给业务模块提供的方法(这就是业务模块提供的服务),从而实现软件的功能。一个软件的好坏与否,与之无直接的关系。
二、业务模块
业务模块,封装了为特定的业务需求提供服务的方法。表现模块就是通过它提供的方法来实现业务需求的。所以,业务模块是直接对应于系统的业务需求的,是系统的关键和最重要的部分。
三、领域模块
也许你会问:既然业务模块已经提供了客户所需的功能,那么还要这个领域模块干什么呢?其实,这个领域模块就是为业务模块服务的更底层的功能。
在整个软件系统中,存在一些实体。这些实体包含了一些数据和责任,它们的交互协作,就可以实现软件系统的业务功能。
实现一个个业务需求的业务模块,可能需要这些实体中的一个或者多个的功能和数据。
这些实体的集合,就是领域模块。由此可见,领域模块实际上才是整个系统的核心和灵魂。业务模块也只有委托它们才能提供系统所需的业务功能。
健壮的企业级应用的一般结构
健壮的企业级应用,在结构上又应该是怎样的呢?
一、表现模块
表现模块仅仅是一个界面,用于向用户提供使用系统的途径而已。所以,尽可能“薄”的表现模块,就是理想的表现模块。“薄”的表现模块,就可以在用户想要改变用户界面时,轻松的加以改变。改变用户界面,是系统变化最多的需求。
使用MVC模式设计的表现模块,可以分为3个组成部分:M(Model)模型,V(View)视图,C(Control)控制器。
其中,模型,是一些类,它们封装了视图所要呈现给用户的数据,也用来将用户操作信息传递给后台的控制器模块。
视图,就是用户界面,是用户看到的那部分。它能够接受用户的请求,并将请求信息发送给控制器,再在控制器完成操作之后,从模型类中获得数据,展现给用户。
控制器,它接收到用户请求之后,就委托业务模块的业务服务对象提供的服务,完成业务功能。如果有数据需要返回给用户,就将数据存放到Model模型类中,以备视图取用。控制器,虽然是表现层的一部分。但是,它实际上是“业务代表模式”的一种应用。所谓业务代表,是指在客户端和业务服务层之间,增设一个“代表层”,所有客户端到服务器的调用,都“委托”该层完成。[9]业务代表虽然身处表现模块内,但实际上执行的是调用业务服务模块功能的任务,完成的是业务功能。因此,不少人都将业务代表划分在业务模块内。我也认同这种划分方法。至少,我们可以认为,它是横跨表现模块和业务模块的一个部分。
表现模块中,模型和视图两部分通常都很小,而且它们是表现模块固有的,不能够省略,就算够“厚”(比如,富客户端技术),也没办法变小。
实际上,我们说企业级应用的表现模块太“厚”,都是指太“厚”的控制器。理想的控制器,只应该根据接收到的界面上不同的请求,调用业务模块的不同业务服务,完成这些请求,然后将得到的数据塞进Model模型类即可。
我们常常犯的错误,就是在控制器中塞进了太多的应该放在业务模块的业务服务类中的代码。实际上,判断控制器是不是太厚,有一个非常简单的方法:假设我们使用另一种表现模块技术,那么,这个新的表现模块中的控制器类中有多少代码和现有的控制器是重复的。如果存在重复代码,就使用“重构”技术,先将它们提炼成方法,然后再移到业务模块的业务服务类中。这样,我们就能够得到一个理想的“瘦”表现模块!
二、业务模块
业务模块,包括2个部分:一个是表现模块的控制器,它是“业务代理”,提供的也是与业务相关的服务。我认为,把它划分在业务模块也许比表现模块更加合适。另一个是业务服务模块,我用Service表示它,它封装了为特定的业务需求提供服务的方法。它与控制器配合,共同完成用户需要的业务功能。
既然理想中的控制器是“瘦”的,而且所有的重复代码都移到了业务服务模块中。那么,理想的业务服务模块必然是“胖”的。
实际上,控制器和业务服务模块,是典型的“客户—服务器模式”。控制器作为客户,需要调用作为服务器的业务服务模块提供的服务,来完成用户需要的功能。所以,服务器越胖,提供的服务越多,那么系统的重复代码就越少,功能也越强大!
三、领域模块
业务服务模块和领域模块,实际上也是典型的“客户—服务器模式”。业务服务模块虽然提供的服务功能强大,很“胖”。但是,它的“胖”也是有限度的!它的“胖”来自于控制器中理应属于它管辖的重复代码。实际上,在“控制器模块—业务服务模块”这对“客户—服务器”关系中,是控制器模块“瘦”,而业务服务模块“胖”。
而在“业务服务模块—领域模块”这对“客户—服务器”关系中,则是作为“客户”的业务服务模块“瘦”,而作为“服务器”的领域模块“胖”。
领域模块,主要就是领域模型(也叫作业务对象Bussiness Object)。领域模型,封装了业务实体的数据,还提供一些处理这些数据的服务(“服务”在编程语言中就是由方法提供的)。一般,在企业级应用中,有一些领域模型需要持久化存储,就是保存到数据库(关系型数据库或对象数据库)、文本文件、XML文件、序列化文件等持久地存储起来,已备下次再用这时,需要持久化的业务对象就需要对应的提供数据访问服务的类(也叫作DAO,Data
Access Object数据访问对象)。
这样,一般的企业级应用的领域模块,主要有两个模块:领域模型(Domain Model)和数据访问服务模块(DAO)。
在“业务服务模块—领域模块”这对“客户—服务器”关系中,应该把业务服务模块中所有可以移到领域模块的领域模型类和数据访问服务类中的代码都移到领域模型类和数据访问服务类中去。
因为,业务模块的控制器和业务服务模块,它们与领域模块的领域模型和数据访问服务模块之间的关系是“多对多”的关系。一个业务模块可以使用零个或者多个领域模块;一个领域模块也可以被零个或者多个业务模块所调用!所以,领域模块越“胖”,提供的服务越多,业务模块就越少重复代码,系统的功能就越强大!
现在,很多程序员都接受了UML的用例驱动开发的思想。诚然,用例驱动开发的思想确实很好,但是很多程序员都由此犯了一个毛病:他们常常按照用例为系统的源代码分包(就是Java中的Package,.Net中的namespace),错误的将领域模块的领域模型、DAO数据访问服务类和业务模块的控制器、业务服务类划分在一个包里。实际上,领域模块和业务模块完全不同,它们并不是从属于某一个用例的,而是属于整个系统的,可以被多个业务模块共同使用的。
如果把领域模块放在首次用到它们的业务模块之中。那么我们就很难在其他业务模块调用它们时很好的使用它们。因为,其他业务模块的使用,可能会要求领域模型类和DAO类增加新的字段和方法。而将它们放在另一个业务模块所在的包里,我们就很难将这些新增的功能放到领域模型类和DAO类中,使它们更“胖”。而是会倾向于在控制器类和业务服务类中增加方法。这样,就会导致各个业务模块中出现重复代码,引发逻辑重复。
怎样开发健壮的企业级应用
企业级应用程序,按照是否能够“随需应变”来划分,可以分为两类:健壮的和脆弱的。
画一根数轴,我把它叫作“软件健壮度”图。“健壮”在正方向,“脆弱”在负方向。有很多技术原理和开发方法,可以让我们的应用程序更加健壮,而违反这些原理和方法,我们的应用程序就会变得更加脆弱,修改和扩充新功能也更加困难。
要想开发出健壮的企业级应用,首先需要的就是开发人员扎实的编程技能和对编程原理的清楚认识和应用。没有高水平的开发人员,而奢谈“健壮的企业级应用”,是毫无意义的。
面向对象编程技术
面向对象编程技术(OOP,object-oriented programming)是近几十年来编程领域最伟大的成就。健壮的企业级应用程序,甚至任何健壮的软件,必须首先是一个很好的实践了OO思想的软件。脱离了OO,就不用奢谈什么合格的软件了!
面向对象编程技术,早已经从昔日的神坛上走了下来。今天,任何一门主流的编程语言都是支持面向对象编程的。现在,几乎所有的程序员都自称已经掌握了面向对象编程的技术。但是,真正掌握OO的程序员却远远没有这么多。而且,面向对象编程技术还在飞速发展的过程中,我们还远没有发掘出它的全部内涵。
并不是说,你使用了面向对象的编程语言开发软件,你就能够开发出实践了OO思想的软件。
要想开发出健壮的企业级应用,我们需要的是全面皈依OO!
面向对象编程的几个原理[2]:
依赖于抽象,而不要依赖于具体实现
具体来说,就是依赖于接口(Interface),而不要依赖于接口的具体实现类。或者是,依赖于父类(最好是抽象的),而不是具体实现的子类。又或者是,依赖于父接口,而不是子接口。总之,只使用提供了所需方法的最基本的类型。这样,当程序需要改变时,我们就可以仅仅提供另一个“服务器”实现类或者实现子类,然后在“客户类”新建实例的地方更换成这个新实现类即可,无须更换“客户类”的调用代码。在使用IOC(反转控制)容器,如Spring框架时,我们甚至可以不用改动“客户类”的任何代码,而只需更改元数据(在Spring
框架中,是简单的Xml文件),将旧的实现类的类名换成新的即可。
使用越是抽象的接口或者类,我们可以选用的实现类也就越多!
我们知道,在Java语言中有这样几种作用域:Private私有,Protected保护,Package包,public公共。按照“依赖于接口Interface”这一原则,我们又有了一个新的作用域:Publish已发布。接口Interface中声明的方法就是已发布的。既然我们现在是通过接口来使用实现类的方法,那么就是说,即使实现类还有其他的Public可见方法,我们也不会调用它们。
比如说,A类实现了B和C接口。调用A类的客户代码中,B b=new A();这样调用,那么我们通过对象b就只能够调用B接口发布(Publish)的代码,这也就保证了不会因为程序员一时的疏忽而造成了代码不必要的耦合。如果需要改变B接口的实现,假设D类实现了B接口,只需要在客户代码中改成这样:B
b=new D();就可以了。
当然,凡事总有例外的情况。对于特别简单,而且不大会改变的类,我们也可以直接使用实现类,而不是接口。如:表现层MVC模式中的Model模型类,还有领域模型类,它们主要是提供数据,只有很少的方法(set/get方法不算),而且不经常变化,所以一般我们直接使用它们,而不使用接口。
使用委派而不是继承
要让一个类实现一些功能,有三种方法:
1,在类中直接写上实现功能的代码。
2,使用继承。让类继承另一个类,从而可以使用另一个类的所有公共的和受保护的方法。
3,使用委派。在一个类中,通过定义对象类型的实例变量,可以调用那些对象的方法,来提供功能。
另外还有一种特殊的使用委派的方式—“回调模式”。就是在类的方法中声明对象类型的参数,然后调用这个参数的方法来提供功能。在使用时,客户类需要提供对象的实例作为参数传给这个类。
这三种方法中,对于没有现存代码的特殊功能,我们可以使用在类中直接写上实现功能的代码这一方法来实现。而在已经存在拥有可以使用的方法的类时,我们可以使用继承或者委派使用它们的方法。
如果我们使用继承这种方式来获得功能,那么我们就会获得可能我们并不需要的大量的父类字段和功能。这样的冗余,就会造成逻辑上的混乱。而且,Java只能够进行单继承,即,一个类只能够继承一个父类。这样,一旦继承了一个类,就剥夺了它继承的能力。
也许你会问,为什么Java要取消多继承呢?因为,多继承提供的好处远比它造成的问题更多。如果2个父类的字段、方法同名怎么办?而且,在逻辑上也会造成极大的混乱。
继承能够做到的事情,委派一样都能够做到。而且做得更好!
我们可以把所需的任务委派给任意多个类(别忘了,应该尽量使用接口Interface,最好用上IOC容器),然后在提供服务的方法中,使用委派对象的方法来实现。这样,在逻辑上,我们能够借助于这些类实现所需要的功能,而没有实现增加不需要的字段和方法。
所以,我们应该牢记“使用委派而不是继承”这一条原则。当然,也还是有场合可以使用“继承”的。
1,一个类和另一个类是纯粹的扩展关系,逻辑上没有半点不符合的地方。如,几何类和方形类。当然,这种情况下,使用委派仍然是可以的,而且更加保险。因为,常常有很多看上去非常像父子关系的类,实际上并不是真正的父子关系。父类可能有几个子类不可能有的方法,这会成为一颗定时炸弹,在我们需要给子类增加相关方法时引发问题。
2,模板方法模式(Template Method)[3]适用的情况下。当知道如何实现一个工作流程,但不知道所有单个步骤被如何实现时,使用模板方法模式比较简单。[4]在父类(通常是抽象类)中提供一个工作流方法,然后再提供几个工作流方法需要用到的方法的抽象方法原型。子类只需要覆盖这几个抽象方法,就能够提供不同的实现效果。这种用法也叫作“多态”。
但是,即使是这种情况,委派仍然能够胜任,而且比继承提供的解决方案更加灵活,只是稍微要复杂一些。这就是Strategy策略模式[3]。策略模式将不变的行为集中到一个接口中。用接口的实现类来实现具体的功能。
“依赖于抽象,而不要依赖于具体实现”和“使用委派而不是继承”这两条原则,其思想都是相同的:不要为类提供不需要的能力。只要正巧够用就行。与其他类通讯时,也只使用正巧够用的服务。只有这样,才能够保证所有符合接口的类能够被自由的替换成其他实现类。
“客户—服务器”关系中,应该是“瘦”客户类,“胖”服务器类
在使用委派的类中。委派的类就是“客户”类,被委派的类,就是“服务器”类(也有人把它叫作“助手类”)。在这样一对关系中,应该尽量给客户类“减肥”,而给服务器类“增肥”。因为,“客户—服务器”关系中,客户和服务器类一般是“多对多”的关系。变胖的服务器类可以在未来给更多的客户类提供更好的服务,而原本会在客户类这边重复的代码就都消失了。
这个原理,就是处理“委派关系”中两个类之间关系的一个原则。
类存在的意义是提供的服务,而非保存的数据
对于类,最重要的是什么,一向都有争议:有的认为,类存在的意义就是封装的数据,有的认为,类存在的意义就是提供的服务(也就是方法)。我赞同后者的观点。类存在的意义就是通过方法提供服务。而类保存的数据也需要通过set/get方法暴露出来。
我们都知道这样一个经典的公式:数据+算法=程序。而类就是同时封装了数据及其相关算法的模块。或者说是算法及其使用的数据的模块。
方法,是很早就出现的一个概念,在面向过程编程时代就是一个核心的概念,是那时最重要的抽象机制,它的出现,使我们第一次拥有了将具体实现代码屏蔽了起来的能力。
方法,提供了一个程序执行的点。在面向方面编程(AOP)中,叫作“连接点(join point)”。我们可以在方法调用的前后进行拦截,增加其他代码。这是AOP面向方面的编程思想,在下面会详细讲解。另外,客户代码调用方法时,只需要给出方法名和参数,并不需要了解方法的实现,这也就给了“客户—服务器”之间解除耦合的一次机会。方法的内部实现可以任意改变,只要不改变方法签名即可。
在OOP中,我们还可以使用这个接口的另一个实现类提供的另一个方法实现版本,来提供不同的服务。
我们知道类是封装了数据和方法的集合。其实,从用例驱动开发的角度来看,是类封装了服务,然后服务需要使用一些数据,就把这些数据也封装在了类中。
我编写类的方式,是“客户—服务器”的方式。使用委派,也就是使用了“客户—服务器”的方式来编程。首先,是用户提供的用例(XP中叫作“用户故事”)。但是用例太宽泛了,不足以支持编程工作的展开。于是,用户再提供每个用例的具体的事件流。在XP中,是提供验收测试,验收测试中也包含了事件流。
事件流,就是驱动我们开发的第一个“客户”。理解了事件流,我们就可以画出UML的序列图。序列图描述了系统的业务模块提供了哪些服务,从而完成事件流。我们可以直接将序列图的逻辑编写成控制器类。控制器就是我们源代码中的第一个“客户”。它和业务服务Service类构成了“客户—服务器”关系,可能也会和领域模块的领域模型类构成“客户—服务器”关系。
在控制器类这个“客户”中,我们已经实现了整个事件流的功能。只不过,有不少要调用的服务(也就是方法,不管是哪一个类的方法,控制器自己的方法,属于Service业务服务类的方法或是领域模型类的方法等)还没有实现。
将这些服务按照逻辑和是否会在客户端造成重复为标准,分配给各个模块的各个类。按照“针对接口”编程的原则,将这些服务分发到各个接口中去,而不是实现类。
现在,虽然程序还没有开发完成,但我们已经知道程序在逻辑上已经完成了,或者说,已经设计完成了。我们只剩下两项简单的实现层面的工作要做:1,编写服务的实现代码;2,利用重构,将这些方法移到最适合的接口和实现类中去。
“服务”是我们所需要的。我们用方法来实现服务。而方法又可能需要一些变量来保存状态,其中有些状态需要使用实例变量来保存。仅此而已!
重构,其实总的思路也是尽量消除变量,特别是实例变量;尽量提炼出方法,而不使用变量。因为,变量是“实现”级别的,是直接的源代码,是死的,不允许变化。而方法是“设计”级别的,是活的,只要方法签名不变,其内部的实现代码可以任意变化。而且,变量不是一个“程序执行点”,不可以拦截,而方法就可以拦截,如AOP面向方面编程,或者代理模式,装饰者模式等的拦截。
按照方法和数据的比例,类可以分为三种类型:只有数据和get/set方法的哑容器类,既有数据又有实际方法的一般类,只有方法没有实例变量的类。
1,只有数据和get/set方法的哑容器类
它们仅仅是数据的容器。Martin Fowler将它们称为“婴儿类” [5]。作为一个起点,是可以的,但是它并不成熟。甚至,我们很难认为它们是真正面向对象的。但是,现实中,还是有不少这样的类存在。比如MVC模式的表现模块中的Model模型。它的任务就是封装将要呈现给客户的数据。还有,领域模块的领域模型类,它封装了业务实体的数据。但是,我们都可以在它们内部封装一些使用这些数据的方法。
总之,碰到这种只有数据和get/set方法的哑容器类,请特别留意,看是否能够重构,让它成为正常的类。
2,只有方法没有实例变量的类
由于方法是编程世界的一等公民,所以这种类型的类是正常的,健康的,在现实世界还是非常普遍的。这些类,是提供了服务,但是服务的实现代码不需要保存实例变量的类。像表现模块(也许说它属于业务模块更贴切)的控制器类,业务模块的业务服务Service类,领域模块的DAO数据访问服务类都是这一类型的类。
这些类有一个优点,那就是它们不怕多线程的应用。因为它们不需要保存特定于线程的数据。所以,我们可以用单例模式来使用它们。即,整个应用程序中,只生成这些类的一个实例,用于为所有用户的请求提供服务。JavaEE的Web容器中,Servlet就是单例的。Spring框架管理的类,也可以使用单例。
单一功能的方法
在面向对象的开发方法中,接口、类、方法都需要对应于单一的逻辑概念。贯彻这一原则,就可以使接口、类和方法的数量变多、块头变小、关系变简单、逻辑变清晰。重构的一大目标,就是将一个大方法编成多个小的单一责任的方法。单一责任的方法,很多都是重构的结果。
OO的委派,实际上就是委派给其他类的方法来提供服务。优秀的OO软件,就是一层一层的方法委派其他类(作为“服务器”的类)的方法来提供功能。其代码的特点就是一个方法内部调用了几个方法来实现功能,这些方法的名字就解释了它们的功能。然后,这些被调用的方法内部又像这样调用了一些方法。循环不已,直到最底层的充当“服务器”的类的方法中是调用API类库的几个方法而告终。
单一职责的接口
一个设计上的逻辑概念,应该有且仅有一个提供对应逻辑的接口。这条原则就是类的内聚性原则:一个模块的组成元素之间的功能相关性。Robert
C. Martin把内聚性和引起一个模块或者类改变的作用力联系起来。[2]
“就一个类而言,应该仅有一个引起它变化的原因”。接口也是类。所以,我们可以说“就一个接口而言,应该仅有一个引起它变化的原因”。实际上,一个实现类,是很难做到“仅有一个引起它变化的原因”的。而接口这个纯设计层面的概念就不同了,它是可以真正做到“仅有一个引起它变化的原因”这一要求的。即使一个实现类能够做到“仅有一个引起它变化的原因”,那也可能会造成实现类太小、太多的问题。而接口只包含方法签名,并没有实现代码,所以,即使存在大量的接口也没有问题。
职责,就是“变化的原因”。如果你能够想到多于一个动机去改变一个类或接口,那么这个类或接口就具有多于一个的职责。
那么,为什么要把两个职责分类到两个单独的接口中呢?因为每一个职责都是一个变化的轴线。当业务需求发生变化时,该变化就会反映为接口的职责的变化。如果一个接口承担了多余一个的职责,那么引起它变化的原因就会有多个。
如果一个接口承担的职责过多,就等于把这些职责都耦合到了一起。一个职责的变化,可能会引起接口完成其他职责的能力被削弱。
比如说,一个业务模块的业务服务模块有两类业务服务,一类是提供增删改查领域模型数据的服务,另一类是判断数据完整性和正确性的服务。那么,这些服务就需要分别存放在两个不同的业务服务Service接口中。这样,在需求变化,从而导致服务改变时,我们可以只改变一个接口,而另一个接口及其所有客户类都不会受到影响。
用接口来隔离实现类
一个设计上的逻辑概念,应该有且仅有一个提供对应逻辑的接口。现实中,我们的实现类可能并不是这么纯正的,可能,我们的实现类同时实现了很多个接口,这样的实现类,叫作“杂凑类”。但是这并没有多大的关系,因为这仅仅是一个“实现”级别的问题。我们仍然拥有一个纯正的接口,我们在使用这个实现类的时候,是通过接口来使用的。这样,杂凑类实现的其他接口的方法,我们并不会使用,也不能够使用。接口的任何实现类都和“客户”调用代码无关!
直接使用编程语言的概念进行设计
软件开发的真正进步依赖于编程技术的进步,而这又意味着编程语言的进步。C++就是这样的一个进步。它已经取得了爆炸式的流行,因为它是一门直接支持更好的软件设计的主流编程语言。
C++在正确的方向上迈出了一步,但是还需要更大的进步。[6]
Java就是这样一门比C++更加先进,更加面向对象的语言。Java可以更加有效的直接支持软件设计。
我们在进行软件设计时,应该直接使用Java的概念来描述软件系统,进行设计。这样,我们就可以直接将设计转化成实现。既然我们主张“针对接口编程”,那么,我们就应该主要使用“接口(Interface)”这个概念来描述系统。另外,既然我们更重视“服务”,也就是方法,那么我们就使用方法,而不是数据来描述接口。
XP提出的CRC图(Class,Responsibilities,Collaboration类、责任、类间关系)很符合我们的要求[7]。CRC图,描述了一个类,我们这里通常是描述一个接口Interface。其中的责任,就是服务,也就是方法。它是接口的内在要求,是接口之所以存在的原因。类间关系,就是接口和其它接口之间的关系。接口之间互相协作,才能够完成业务功能。
我们在进行软件设计时,不要考虑设计的具体实现的细节问题。我们只需要考虑接口应该提供那些服务,以及和哪些接口协作,怎样协作即可。
尽量使用模式来解决问题
内行的设计者们都知道:不是解决任何问题都要从头做起。它们更愿意复用以前的解决方案。当找到一个好的解决方案,他们会一遍又一遍的使用它们。这些经验是他们成为内行的部分原因。因此,你会在很多面向对象系统中看到类和相互通信的对象的重复模式。这些模式解决特定的设计问题,使面向对象设计更灵活、优雅,最终复用性更好。它们帮助设计者将新的设计建立在以往工作的基础上。服用以往成功的设计方案。一个熟悉这些模式的设计者不需要再去发现它们,而能够立即将它们应用于设计问题中。[3]
给“模式”所下的定义是这样的:
自从1994年,GoF的划时代名著《设计模式—可复用面向对象软件的基础》问世之后,在程序员世界引起了轩然大波。众多的模式著作纷纷推出,涉及各个领域。其中,Martin
Fowler的《分析模式:可复用的对象模型》和《企业应用架构模式》,以及Sun Java中心编写的《J2EE核心模式》都是其中的上乘之作。另外还有很多特定领域的模式著作。它们搜集了特定领域的一些模式。
今天,简单的浏览这些模式书籍,我们就可以得到一大堆的专家级解决方案和经验。
实际上,使用模式来解决问题,并不需要你精研很多种模式,只需要你大致了解它们使用的场合,能够在遇到这类问题时想起应该到哪里寻找对应的模式即可。模式,仅仅应该作为一个字典。
但是,《设计模式—可复用面向对象软件的基础》这本书还是应该精心研读。因为这本书中的23种模式,是最重要,应用最广泛的模式。
当然,凡事都不可太过,过犹不及!现在,有一些程序员凡事都以模式马首是瞻,不分场合都要套用模式。我们应该注意到,用模式来解决问题,一般情况下都会增加软件的复杂性。本来,一个类解决的问题,现在需要几个类协作才能够解决。所以,对于明显简单的功能,不宜使用复杂的模式。另外,一个软件中不宜使用太多的模式。否则,会产生大量的类和类间的关系,使系统过于复杂。
面向方面编程技术
面向方面编程AOP(Aspect-Oriented Programming)是1996年Gregor Kiczales在PARC提出的一种新的编程范式。AOP是一种与OOP截然不同的看待应用程序结构的方式,按照AOP的观念,系统被分解为方面(aspect)而不是对象。
OOP是一种成功的、极具表现力的编程范式,很多领域概念都可以自然的表达为对象,从而将其中通用的代码模块化。然而,还是有OOP照顾不到的角落。
衡量OOP成功与否的标准就是它在多大程度上避免了代码重复。
代码重复是最糟糕的代码臭味。只要出现重复的代码,必定有什么地方存在严重的问题—要么是设计有问题,要么是实现有问题。
一般情况下,OOP能够很好地避免代码重复。具体继承可以帮助我们在不同类型之间共享相同的行为;多态让我们可以用同样的方式处理不同类型的对象,将注意力集中到它们的共同之处。
而委派,使类之间构成了“客户—服务器”关系。客户类代码简单的调用服务器类的方法即可完成众多的功能“委派”。
但是,有些时候,我们无法用OOP避免代码重复,或者无法用OOP得到一个优雅的解决方案。
就拿日志记录来说吧。假设我们要对每一个控制器类的每一个方法调用都进行日志记录。那么,使用OOP委派的编程风格,也就是使用“客户—服务器”调用模式。我们需要在软件的每一个控制器类的每一个方法中都增加一行调用Log日志记录的代码。尽管OOP委派已经很好的将记录日志的服务封装在作为“服务器类”的Log类中,但是,作为“客户—服务器”调用模式,总是不能够去掉客户类中调用服务器的服务的代码。如果很多个“客户类”都需要调用“服务器类”提供的“服务”,那么“客户类”中的调用代码,就会不可避免的存在大量的重复。这是“客户—服务器”这种客户主调模式不可避免的弊端。
当然,老实说,这类“客户类”中的重复出现的调用代码,并不是什么大不了的问题。存在这些重复代码的软件,照样可以是一个比较健壮的软件。这也正是我把OOP面向对象编程技术作为开发有效软件的第一技术的原因。
但是,今天,AOP面向方面编程提供了另一种调用模式,使客户类能够不需要调用代码,就能够获得所需的功能!这样,就完全消除了重复代码。
AOP的调用模式,就是好莱坞原则:“不要试图联系我们,我们到时候自会通知你。”这完全不同于OOP委派的“客户—服务器”调用模式。OOP中,如果一个类(客户类)需要另一个类(服务器类)提供的服务,就需要在客户类的源代码中显式地加上调用代码。而在AOP的“好莱坞原则”中,如果一个类(客户类)需要另一个类(服务器类)提供的服务,那么就不需要在客户类中作任何需要服务的声明。而是在服务器类中指定需要将服务提供给哪些客户类。
用我们现实生活中的事情做个类比:
我需要坐出租车,那么我就需要亲自打电话去给出租车公司,调用它们的“服务”。这就是OOP委派的“客户—服务器”调用模式的工作原理。出租车公司提供专业化的出租车服务,客户只需要简单的调用它们提供的服务。
或者,我走在路上,随地扔了一张废纸。然后,清洁工会将这张废纸捡走。这里,我并没有主动要求清洁工提供服务,而清洁工自己为我提供的服务。
又或者,我每天走在公路上。但是,这条公路并不是我建造的,它的维修也和我无关。
清扫垃圾,维修道路、桥梁,实际上是政府为公民提供的一项服务,或者叫作“基础设施”,不管是有形的基础设施,还是无形的基础设施。
政府提供“基础设施”(如:清扫垃圾,维修道路等),和AOP的“好莱坞原则”原理是一致的。我把“好莱坞原则”称作是“客户—基础设施”调用模式。“客户”,就是实际上需要服务的类。“基础设施”,就是为客户封装和提供服务的类(AspectJ这样直接支持AOP的语言中,叫作“方面”,而在像SpringAOP这样的OOP语言的AOP框架中,还是用一般的类来表示“基础设施”)。“客户—基础设施”调用模式中,是“基础设施”自己作用于“客户”。“客户”根本不知道有“基础设施”的存在!
基础设施,又分为两部分:封装服务的类和指定该类为哪些客户类的哪些方法服务的部分。
AOP的重要概念
首先,让我们来澄清AOP中的各个重要概念的定义。由于AOP兴起时间不久,而且流派众多,再加上国内翻译又各异,所以在这些概念的定义上有很多不同的版本。[8]
一、关注(concern)
一个关注可以使一个特定的问题、概念、或是应用程序的兴趣区间。或者说,是“涉众”对应用程序的期望。涉众,就是与程序有关的人或者物,如客户,程序员,关联系统,客户端等应用程序的环境中的某些部分。总而言之,是应用程序必须达到的一个目标。
日志、安全性、事务管理、性能要求、用户管理、角色管理、角色授权管理等等,都是系统中常见的“关注”。在一个OO的应用程序中,关注可能已经被代码模块化了,也可能还散落在整个对象模型之中。
实际上,“关注”不是AOP独有的概念,应该是应用程序都有的一个概念。它有些类似于面向对象的UML图中“用例”,或者是XP的“故事”。
二、横切关注(crosscuting concern)
如果一个关注的实现代码散落在很多个类或方法之中(如:日志、安全性检查),我们就称之为“横切关注”。
如果用OOP实现横切关注,那么必然会造成调用代码重复。如果我们使用AOP实现横切关注,就可以让客户类中不必进行任何代码调用。
横切关注,正是AOP的用武之地。
三、方面(aspect)
一个方面是对一个横切关注的模块化,它将那些本来散落在各处的、用于实现这个关注的代码规整到一处。它一般包括两个模块:封装服务代码的模块和指定该服务为哪些客户类的哪些方法服务的
模块。
四、连接点(join point)
程序执行过程中的一点。如:
1,方法调用(method invocation):对方法(可能包括构造器)的调用,不过并非所有AOP框架都支持在对象构造时的增强(advise)。
这是最重要,最常用的连接点。我们在面向对象编程技术中曾经说过,重构的一大手段和目标就是构造大量的方法。因为,方法是一个连接点,是一个抽象,我们可以利用方法这一层抽象,任意的修改方法内的实现代码。所以,我们的代码中应该是大量存在方法这个兰接点的。即使没有,我们也可以在应用AOP编程时重构,在需要拦截的地方重构出一个方法,来作为连接点!
有些AOP实现,如JBoss AOP,就只提供了方法调用这一种连接点。
2,字段访问(field access)
读或者写实例变量。同样,并非所有的AOP框架都支持对字段访问的增强。那些支持这类增强的AOP框架都可以区分读操作和写操作。
Spring AOP,JBoss AOP都不支持字段拦截。字段拦截是一种潜在的危险,它违反了OO的封装原则。
一般来说,我认为最好还是不要使用字段增强。OO程序中对字段的访问,可以用set/get属性这样的方法调用来代替
AOP对字段的拦截,通常也可以通过方法层面的增强来代替,从而保持对象的封装。
3,异常抛出(throws)
特定的异常被抛出。JBoss AOP框架只支持方法调用。但是,仍然可以通过编程获得异常抛出。实际上,异常抛出这个连接点,是方法调用这个连接点的衍生品。能够拦截方法,那么一定能够拦截抛出的异常。
五、增强(advice)
这个术语有很多种译法,罗时飞在《精通Spring》一书中译作:“装备”;Spring中文论坛在《Spring Framework开发参考手册》中译作“通知”,石一楹在《expert
one-to-one J2EE Development without EJB中文版》一书中译作“增强”。这里,我就把它称作是“增强”吧!
增强(advice),是在特定的连接点执行的动作。很多AOP框架都以拦截器(interceptor)的形式来表现增强—所谓拦截器,就是这样一个对象:当连接点被调用时,它会收到一个回调消息。增强的例子包括:
1,在允许执行连接点之前,检查安全凭证。如Spring框架的一个附属开源项目Acegi,就是这样一个使用Spring AOP拦截方法访问的项目。
2,在执行某个方法连接点之前开启事务,在连接点执行完毕后提交或者回滚事务。Spring AOP框架提供了这个功能。
六、切入点(pointcut)
一组连接点的总称,用于指定某个增强应该在何时被调用。切入点常用正则表达式或别的通配符语法来描述。有些AOP实现技术还支持切入点的组合。
切入点加上增强,就是一个完整的方面,或者叫作“基础设施”。可以实现横切关注。
七、引入(introduction)
又译作“混入”。指,为一个现有的类或接口添加方法或字段。这种技术用于实现Java众多多重继承,或者给现有的对象模型附加新的API。譬如说,可以通过引入让一个现有的对象实现一个接口。
八、混入继承(mixin inheritance)
一个“混入类”封装了一组功能,这组功能可以被“混入”到现有的类当中,并且无需求助于传统的继承手段。在AOP这里,混入是通过引入来实现的。在Java语言中,可以通过混入来实现多重继承。
九、织入(weaving)
将方面整合到完整的执行流程(或完整的类,此时被织入的就是引入)中。这是AOP的实现机制,AspectJ是使用预编译器,在编译之前通过生成代码实现织入的,这叫作“静态织入”。
Spring AOP等AOP框架是在运行时通过动态代理生成匿名类的匿名对象的方式织入的。这叫作“动态织入”。
十、拦截器(interceptor)
很多AOP框架(例如Spring和JBoss 4,但不包含AspectJ)用它来实现字段和方法的拦截(interceptor)。随之而来的就是在连接点(譬如方法拦截)处挂接一条拦截器链(interceptor
chain),链条上的每个拦截器通常会调用下一个拦截器。实际上,拦截是一种AOP的实现策略,而不是AOP的核心概念。
十一、AOP代理(AOP proxy)
AOP框架创建的对象,这个匿名类的匿名对象,它既委派目标对象完成目标对象的工作,也织入了拦截连接点的通知。在Spring AOP中,AOP代理可以是JDK动态代理或者CGLIB代理。
AOP代理也并不是所有AOP实现都有的一个概念,它是Spring AOP框架和JBoss AOP框架等动态AOP框架爱实现AOP的根本方法。
实现AOP的主要技术策略
AOP面向方面编程思想目前已经有很多种实现和实现方法。下面是用于实现AOP的主要技术策略:
一、J2SE动态代理
在Java中,实现AOP最显而易见的策略莫过于使用Java1.3引入的动态代理。动态代理是一种强大的语言结构,它使我们可以为一个或多个接口“凭空”地创建实现对象,而不需要预先有一个实现类。
如果需要用动态代理实现环绕增强,可以在其中调用必要的拦截器链。拦截器链上的最后一个拦截器将借助反射调用目标对象—如果有目标对象的话。
动态代理最大的好处在于:这是一种标准的Java语言特性。除了AOP框架之外不需要第三方库,也不会受到应用服务器的任何影响。
动态代理的最大局限性在于:它只能针对接口进行代理,不能针对类。即,需要“客户—基础设施”中的客户类实现所需的接口,然后在程序中使用接口来使用新的对象的方法。不过,既然我们主张“针对接口编程”,那么这项限制并不是坏事,反而能够使程序员养成良好的“针对接口编程”的习惯。
另外,动态代理只能对方法调用进行增强,而不能像AspectJ那样对字段进行增强。不过,既然我们使用方法来提供所有的服务,那么“对字段进行增强”这项功能也就是完全无用的,反而会引起程序员使用不良的编程方法。
Spring框架,默认时使用J2SE动态代理提供AOP实现。开发者也可以指定使用“动态字节码生成”技术来实现AOP。Nanning框架也使用J2SE动态代理提供AOP实现。
二、动态字节码生成
为了针对Java类提供代理,我们需要动态代理之外的工具,那就是动态字节码生成(dynamic byte code generation)。在这方面,最流行的工具是CGLIB(Code
Generation Library)。在Spring中,如果需要针对类(而不是接口)提供代理,就会用到CGLIB。它可以针对指定的类动态生成一个子类,并覆盖其中的方法,从而实现方法拦截。
不过CGLIB有一个小问题:因为它是通过继承来实现代理的,所以无法为final方法提供代理。
三、Java代码生成
最笨的方法,就是让容器生成新的源码。这种方法,最早大概是MTS微软事务服务器采用的,后来的Java的EJB也采用了这种方法来提供“基础设施”。虽然这种方法很笨。但是,它们确实是早期的AOP尝试。现在,随着动态代理和动态字节码生成技术的出现,这种做法正在逐渐退出流行。
另外,不得不补充一句:代码生成,基本上都是最糟糕的编程技术。通常,我们都可以使用OOP的委派或者AOP来达到相同的目的。自动生成代码,将会在需要修改代码时引起众多可怕的问题!微软是用了不少自动代码生成技术,另外MDA模型驱动开发方法和基于元数据的产生式编程都是常见的源代码生成技术。对于这些技术,我一直都抱着怀疑的态度!只要还有其他的方法可以实现目标,我是绝对不会使用源代码生成这种技术的!
四、使用定制的类加载器
使用定制的类加载器,我们可以在一个类被加载时自动对其进行增强。即便用户使用new操作符构造实例,增强仍会生效。JBoss AOP和AspectWerk都采用这种做法对类进行增强,具体的增强信息则是在运行时从XML配置文件中读取。
这种做法的风险在于:它偏离了Java的标准。在某些应用服务器中,这种做法可能会导致问题,因为J2EE服务器需要控制整个类加载的层级体系。
五、语言扩展
如果我们希望把方面当作一等公民来对待,就需要一种同时支持AOP和OOP的语言。为了达到这个目的,可以对现有的OO语言进行扩展,就像C++扩展C语言、引入OO的概念那样。最早出现的AOP实现AspectJ就对Java进行了这样的扩展。
AspectJ是功能最强大的AOP实现。但是,它是一种新的语言,语法比Java更复杂。而且,还需要使用AspectJ的预编译器首先编译AspectJ源码,将它们变成增强后的Java代码,然后再进行Java的编译,太过繁琐。
Spring AOP框架
在所有的AOP实现中,我认为Spring AOP框架是最好的选择。尽管它使用的动态代理和动态字节码生成技术实现的AOP功能并不是最强大的,但是对于大多数情况已经够用,而且够简单,没有任何特殊的要求。
并且,Spring框架很好的整合了AspectJ,在必要的时候,我们可以使用AspectJ的强大能力来实现AOP。
Spring AOP是用纯Java实现的,它不像AspectJ那样需要特殊的编译过程,也不需要像JBoss AOP那样需要控制类装载器层次,因此适用于J2EE容器或应用服务器,也适用于任何使用Java的程序。不过,你的程序也必须要使用Spring框架来管理。Spring
AOP只能够为Spring框架管理的Java类提供AOP服务。
Spring目前只支持拦截方法调用和异常抛出,不支持拦截字段访问。
Spring提供代表切入点或各种通知类型的类。Spring使用术语advisor顾问来表示代表方面(aspect)的对象。它包含一个增强(advice)和一个指定特定连接点(join
point)的切入点(pointcut)。
Spring AOP框架的目标并不是提供极其完善的AOP实现(虽然Spring AOP非常强大),而是提供一个和Spring
IOC容器紧密结合的AOP实现,帮助解决企业级应用中的常见问题。
因此,Spring AOP的功能通常是和Spring IOC容器联合使用的。AOP通知是用普通的bean定义语法定义的。增强和切入点本身由Spring
IOC管理—这是一个重要的和其他AOP实现的区别。
有些事使用Spring AOP是无法容易或高效的实现的,如非常细粒度的对象,此时可以使用AspectJ。
总的来说,Spring针对J2EE企业级应用中大部分能用AOP解决的问题提供了一个优秀的解决方案。
如何更好的使用AOP
面向方面编程(AOP)为软件开发提供了一种全新的视角—横切的视角。让我们看待软件的视角从纵向的一维世界,变为平面的世界。也大大提高了我们开发软件的能力。最近,AOP将会取代OOP的论调层出不穷。
的确,AOP是一种不错的编程范式和思考方法,但是,OOP才是编程的根本。AOP只是OOP的有益补充,它们之间不是对立关系,而是互补的关系。AOP绝对不会,也不可能替代OOP。两者各有各的领地,而OOP的应用范围更大!
如果一个软件,连最起码的OO原则都没有遵循,又怎么可能奢望依靠AOP来达到健壮的目的呢!
现在,在一些程序员中有一种倾向,就是在编程中一门心思的应用AOP来编程。只要发现任何调用代码重复,都要使用方面来解决。
这就造成了程序中使用了太多的方面。整个程序的逻辑变得难以理解和修改。
我认为,只应该在出现大量调用代码重复的情况下,才应该使用AOP的“客户—基础设施”来解决,否则应该使用OOP的“客户—服务器”模式。对于难以决定应该使用OOP还是AOP的场合,应该使用OOP。
面向关注软件开发
面向对象编程技术,是一种解决业务关注的编程技术。比如说,一个企业级应用,用户需要用户管理的功能,同时这个软件的所有模块都需要事务处理功能,日志功能和安全性检查的功能。
使用OO技术时,当我们要编写实现用户管理功能的代码模块时,我们得到的系统关注(UML称作“用例”,XP称作“用户故事”),是:用户管理模块,需要有增加、删除、修改和查询用户的功能。同时,所有的数据库操作要有日志,要使用事务,要检查安全性,只有符合条件的用户才能够调用这些管理功能。
然后,我们根据这个业务关注,编写出满足这些要求的软件模块。其中,日志、事务、安全性检查代码都与其它业务关注的实现模块出现了代码重复。
可以说,OOP是用纵向的一维的视角来看待软件系统的。
AOP则是一种解决横切公共关注[9]的编程技术。对于上面例子中的日志、事务、安全性检查等公共的关注,我们不是把它们分散到各个业务关注中,而是集中在一起,构成“横切公共关注”,使用AOP的技术,在一个模块,也就是方面内实现,各个业务关注根本不用知道还存在这些关注,当然,在实现业务关注的OO代码中也不会体现这些横切关注。
我们可以看到,不论是OOP还是AOP,都只是一种编程的范式,是面向关注,解决关注的一种手段。
但是软件开发的根本是什么呢?就是“涉众”的“关注”。软件就是要满足涉众的关注,满足了涉众的关注,那么这个软件就成功了!
没有“关注”,OOP和AOP就都成了无本之木,无源之水,没有目标的箭。而企业级应用,最大的变数和风险,就是客户的需求的变化,也就是系统“关注”的变化。面向关注的软件开发(COSD,concern-oriented
Software Development),让我们始终面向涉众的“关注”这个根本的目标来开发软件。也让我们在“关注”的旗帜下,综合运用OOP和AOP编程技术,分别解决纵向的业务关注和横切的公共关注,相辅相成。
面向关注软件开发(COSD)的思想,是来自于AOP提出的概念“关注(concern)”,是重新审视OOP和AOP相互之间关系的思考结果。
敏捷开发方法的代表作—XP
极限编程(Extreme Programming,简称XP)是目前讨论最多、实践最多、争议也是最多的一种敏捷开发方法。XP是一套能够快速开发高质量软件所需的价值观、原则和活动的集合,使软件能以尽可能快的速度开发出来并向客户提供最高效益。
XP的前提
极限编程的假设:平滑成本曲线—变化的成本不会随时间的推移而急剧上升。而非传统中普遍认为的“变化的成本随时间的推移而以指数方式上升”[7]。传统的计算机科学家认为,对软件的改变,会随着项目时间的推移而变得越来越困难,其成本将是在项目开始初期就将这些需求考虑进来的几十倍。这在过去,确实是软件开发的事实。
但是,随着面向对象编程技术的发展,这一个假设已经变得不符合实际情况了!具有良好面向对象编程技术功底的程序员,完全可以开发出高内聚、低耦合,结构简单,易于修改的健壮的软件,其“成本曲线”是“平滑的”,而非“陡峭的”。即使在软件完成之后,对软件进行大规模的修改和添加新功能,也仅仅比在项目开始的时候明确定义这些需求增加极少的成本。
所以,如果要使用XP取得成功,首先就需要能够掌握本文前面章节介绍的那些方法,让软件的健壮度更高。可以说,XP对开发人员的要求是比较高的,需要有扎实的编程水平和面向对象的思想。
为什么需要XP
那么我们为什么不在项目开始之前就确定所有的需求,而要在项目进行,甚至已经完成的时候修改软件呢?如果能够做到这一点,当然好,但是,现实中,我们常常无法做到这一点。
1,程序员对于开发软件是专家,但却不是软件应用领域的专家。而用户虽然是软件应用领域的专家,但是他们往往不清楚软件能够做到什么,不能够做到什么,或者做一项工作的成本如何。由于开发人员和用户对相互领域的不了解,所以在软件实际开发出来之前,开发人员和用户都无法清楚的定义软件需要的功能。
往往直到软件被实际开发出来的,用户试用之后,才能够明白自己到底需要怎样的软件。特别是在企业级应用中,软件开发完成之后,大规模的修改软件是常事。
2,随着时间的推移,软件应用的环境本身就在发生变化。特别是在企业级应用中,企业的业务环境一直处于不断的变化之中,这就要求企业级应用也要能够适应企业的变化。而这些可能的变化,对于开发者来说,是很难预见的。
3,一般的软件开发过程中,我们往往选择分多个版本、阶段开发软件。就是首先开发一个初级的版本,发行。之后再在其基础上添加其他功能。这是一般的软件开发的策略。这也要求,我们不可能在软件开发的初期就能够考虑到未来若干年内我们软件的全部需求。比如,Windows操作系统,这个软件已经历时十多年,发布了无数个版本。显然,最初开发Windows的时候是不可能预见到今天的Windows的。
总之,软件开发过程中,需求的变化是必然的,绝对的。不变的需求是暂时的,相对的。
面对不确定的未来,以前,我们一般基于“变化的成本随时间的推移而以指数方式上升”这一假设,在项目开发的初期,想方设法的预测未来的种种变化,试图抓住“不确定的未来”。但是,这常常是徒劳的。而且,这么做的结果,往往是把本来简单的需求复杂化了。其中的很多需求,根本就是客户所不需要的。
譬如说,现在很流行远程方法调用。很多程序员想当然的认为软件将来需要提供远程调用这样的功能。他们花了大力气来使用复杂的CORBA,Web
Services,RMI,EJB等技术提供远程调用功能,大大增加了软件的开发成本,而实际上,大部分的软件最后都没有这样的需求。而且,即使将来真的需要提供远程调用的功能,对于设计良好的软件,到时候再增加这种功能也非难事。
简单的软件被搞复杂了,而客户却需要为并不需要的功能埋单!
融合XP的软件开发过程
如果我们在软件开发中贯彻了那些开发健壮软件的编程技术,使我们的软件比较健壮,也就是说符合“平滑成本曲线—变化的成本不会随时间的推移而急剧上升”这一实践XP的前提,那么我们就可以使用极限编程方法来开发软件,提高软件开发的生产效率和商业价值。
获取需求
传统的软件开发方法把这个阶段称作“需求调研”。因为,按照传统的方法,是开发人员调查研究软件的需求。而XP不同,XP是让客户在软件开发人员的引导下为开发人员提供软件的需求。因为,程序员对于开发软件是专家,但却不是软件应用领域的专家。显然,对于软件应该是什么样的,客户是更有发言权的。软件的需求,理所当然应该由客户来提供。
一、初步提出软件需求
客户提出软件应该实现的功能,而开发人员则告诉客户这些功能是否能够实现,以及需要花费多少成本。由客户最后决定需要实现哪些功能。
这个阶段,客户提出的需求,不必深入到细节,只需要一个个粗略的需求。如果深入到细节,那么就会陷入传统开发方法过份需求调研和分析的陷阱中。
XP实践之:小版本—将一个简单系统迅速投入生产,然后以很短的周期发布新版本。
XP将整个软件分成多个小版本,多阶段发行。客户根据功能的优先级将功能划分到多个版本中分批予以实现。但是,第一个版本,如果还没有一个软件的架构,那么这个版本需要实现哪些功能由开发人员决定。开发人员选择那些便于搭建架构的功能,比如用户管理,权限管理等功能,首先实现。
二、版本开发中的软件需求
初步提出的软件需求是大致的,概括的,还没有深入软件需求的细节。进入版本开发之后,我们需要客户提供足够的需求细节,以支持“需求驱动开发”。
比如说,初步需求阶段用户管理这样的需求,在版本开发阶段,需要明确怎样的用户管理功能,比如,需要有增加、删除、查看、修改用户信息的功能。
用UML表示,就是“用户管理”用例。增加、删除、查看、修改用户这四个详细的子用例,或者说是四个事件流。一旦程序员得到了确定的事件流,就可以以此为依据,画出UML的“序列图”,驱动软件开发的展开。
三、横切公共关注
开发人员从客户提供的系统需求中,提取出公共的系统需求,譬如说,日志,访问控制等。对于这些横切公共关注,我们可以使用OOP的方式实现。编写一个类来提供这些服务,然后在各个业务模块中通过“客户—服务器”模式,调用这个类的方法来使用这些公共关注的服务。
或者,更进一步,使用AOP的方式实现。编写方面来提供这些服务。通过“客户—基础设施”这样的调用模式,在业务模块并不知道的情况下,为它们提供这些服务。
测试驱动开发和验收测试级重构
极限编程的哲学思想是实证主义的哲学思想:“任何不能度量的事物都是不存在的”[4]。极限编程反对传统开发方法重视过程和文档的做法,主张快速的开发、构建、测试和部署软件。认为,一切决策都应该基于软件的实际运行结果,而不是无端的猜测。经常“问问电脑”[8],而不是基于一种毫无证据的信念,这是XP人的基本编程原则。
XP人首先就需要是一个皈依实证主义哲学的信徒。
因此,XP的开发方法和过程,可以用这样的公式来表示:
软件=测试驱动开发+重构。
一、验收测试驱动开发
怎样才能够证明开发人员已经实现了一个客户需要的功能呢?这就需要测试—验收测试。验收测试,是在用例实现之前由客户在开发人员的帮助下编写的。验收测试,就是一段文字,用来验证系统是否已经满足客户需求。
开发人员可以根据这段文字,用规范的脚本语言来形式化的定义。通常,我们为每个应用程序编写一套脚本语言。一般,我们使用XML文件的形式,或者是文本文件的形式。开发人员自己编写一个简单的程序,读取这个验收测试的文本文件,根据文本文件中的内容,调用软件中相应的方法,予以执行。脚本语言和具体的方法之间的对应关系,由开发人员自己定义。
实际上,验收测试[2],就是程序的一个用户界面,是基于文本文件或者XML文件的一个用户界面。是调用软件核心业务逻辑的一个简单界面。通过它,我们能够知道软件的核心业务逻辑是否运行正常。
验收测试是程序,因此是可以运行的。我们在软件开发的过程中可以经常运行这些验收测试,以保证软件一直处于正确的状态。
而且,验收测试没有用户界面,可以和单元测试一样批量快速运行,把结果保存到文件里。
验收测试是关于一项软件功能的最终可执行文档。程序员可以阅读这些验收测试来真正的理解这些功能,从而明白应该怎样来实现这些功能。验收测试,本身就是“事件流”,程序员可以据此画出“序列图”,实现验收测试驱动的软件开发。
通过验收测试,驱动软件开发,是XP推荐的软件开发方法。
二、“客户—服务器”模式层层委派开发软件
我们使用“客户—服务器”模式,通过层层委派的方式来编写软件。
序列图描述了系统的业务模块提供了哪些服务,完成了事件流。我们可以直接将序列图的逻辑编写成控制器类(或者叫作“业务委派”, 所谓业务委派,是指在客户端和业务服务层之间,增设一个“代表层”,所有客户端到服务器端的调用,都“委托”该层完成。[10])。
控制器就是我们核心业务逻辑的第一个客户。它和业务服务类构成了“客户—服务器”关系,可能也会和领域模块的领域模型类构成“客户—服务器”关系。
为了实现用户请求的功能,我们在控制器中调用几个方法来实现这个用户需求。但是,现在我们还没有任何可以使用的方法。我们现在照样在控制器中写上要调用的方法,尽管它们还没有被编写和实现。没关系,以后会实现的!
将这些方法按照逻辑和是否会在客户端造成重复为标准,分配给各个模块的各个类。按照“针对接口”编程的原则,将这些服务分发到各个接口中去,而不是实现类。
我们可以使用CRC图来帮助完成这个分配方法的工作。
现在,虽然程序还没有开发完成,但我们已经知道程序在逻辑上已经完成了,或者说,已经设计完成了。我们只剩下两项简单的实现层面的工作要做:1,编写接口的实现类,实现所需的方法;2,利用重构,将这些方法移到最适合的接口和实现类中去。
三、验收测试级重构
重构,就是“在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内在结构”[5]。一旦编写好代码,对代码的任何修改,都可以算是重构。
重构总是和测试联系在一起的。没有自动化测试的支持,是不可能进行有效的重构的。按照所需要的测试,重构可以分为两类:
一是“单元测试级的重构”,也是大家使用最为广泛的重构。但是,这类重构改善的只是软件局部的质量,一般就是一两个类的改善。
另一种是“验收测试级的重构”,这是改善整个模块的质量的重构。它是对整个模块实现机制的改善,常常涉及到多个接口和类的修改。
在需求变化的过程中,依靠自动化的验收测试,我们可以很容易的进行验收级重构,快速地响应软件需求的变化。也可以使软件变得越来越健壮。
单元测试驱动开发和单元测试级重构
在编写具体的类时,我们也使使用“单元测试驱动开发”的。首先编写测试类,然后再编写实现类,逐个方法的完成单元测试,最后完成类的编写。在需要改善类的实现代码时,使用单元测试级的重构,改善类的内部结构。
XP开发软件,就是自顶向下,一层层的由测试驱动开发,在需要改善使,使用这些测试安全的进行重构。
小结
最后,整个软件的开发工作和软件质量,都可以通过验收测试和单元测试得到保障和度量。客户可以随时监控软件开发的进展情况。客户花的钱可以直接看到成果。而且,通过小版本快速迭代的发布,客户可以及早的享用软件开发的成果,也可以即时地终止不合算的软件项目,而不会造成投入全部付之东流。
使用Java开发企业级应用的参考架构
现在,让我们使用Java实际的开发两个企业级应用的参考架构。这个架构并不实现特定的业务需求,仅仅是两个开发企业级应用的架构。这个架构是健壮的,易于修改的,也是符合“平滑成本曲线—变化的成本不会随时间的推移而急剧上升”这一XP要求的。
JavaEE
Java EE,Java平台企业版(Java Platform Enterprise Edition), 是Sun公司为企业级
应用推出的标准平台。Java平台共分为三个主要版本Java EE、Java SE和Java ME。原来,它们叫作J2EE、J2SE、J2ME。
Java EE是使用Java进行企业开发的一套扩展标准。Java EE平台提供了多层、分布式的应用模型,重新利用组件的能力,统一安全的模式以及灵活的处理控制能力。Java
EE包括EJB, JTA, JDBC, JCA, JMX, JNDI, JMS, JavaMail, Servlet, JSP等规范[10]。
“经典”的JavaEE架构
通常,人们认为Java EE是一种分布式平台,EJB在该平台中占有中心地位。“经典”的Java EE架构是这样的[7]:
1,Java类分散在不同的JVM中,虽然要想把这些类并置在一起、消除远程调用的程中负载也是可以办到的。
2,Web层一般由一个MVC架构实现(其他架构方案也都是如此)。如果应用系统设计的比较精心,那么就会有一个专门的客户端代理层,通过它调用远程EJB,这样就能够干净利落的解除web层和EJB之间的耦合。
3,所有的业务对象都是带远程接口的无状态session bean,运行在EJB容器中。EJB容器提供远程调用机制、事物管理、线程管理,可能还有基于角色的安全服务。
4,所有的数据访问都要通过entity bean。Entity bean提供了O-R映射。
5,EIS层由一个或多个数据库或者遗留系统组成。如果存在多个带事务的资源,通过EJB容器访问的JTA服务会负责协调分布式服务。
对于不准备提供EJB集群服务或者远程调用EJB的企业级应用,也可以使用本地EJB。
Java EE的这种架构提供了强大的远程调用能力、JTA声明式事务和声明式安全等服务,以及集群部署EJB的能力。从而使Java在短短几年时间内占领企业级应用市场。
但是,这种架构在多年的实践中,被发现了不少弱点:
1,这种架构开发起来十分困难和低效。这主要是EJB规范太过复杂。按照EJB2.1规范的定义,EJB组件必须事先很多的接口, 比如Home接口、Remote接口、local
接口,等等.还要针对各种应用类型定义许多的xml描述文件。当我们需要访问某个组件或者服务的时候,必须通过JDNI查找,通过名字绑定服务,才能找到我们需要的对象。
2,运行起来也是十分低效的。Entity Bean的实现机制有问题,导致了Entity Bean访问数据库十分低效。甚至,目前很少有人采用Entity
Bean来访问数据库。但是,如果不使用Entity Bean,Session Bean
使用其他数据库访问方式,则会存在一些问题。因为有些数据库访问机制,如Hibernate的Session Factory必须是单个实例的,而Session
Bean中是无法实现单例的。
另外,EJB的执行效率也比POJO慢。
过去,人们认为EJB是Java EE的核心。但是,实际上,所有的Java EE服务,包括JTA,RMI,JNDI,JMS等,都可以脱离EJB容器来使用,它们实际上都是Web容器提供的功能,EJB容器只是提供了一种访问这些服务的方法而已,我们完全可以不通过EJB而直接使用这些Java
EE服务。
Java开源软件
Java是一种开放的技术,它的发展,既由官方的JCP(Java Community Process)组织通过制定规范推动,也由Java开源社区推动。战斗在一线的程序员,开发出了很多开源的Java软件,这些软件得到了很大的应用,极大地推动了Java的发展。我将使用这些开源软件,结合Java
EE提供的服务,不使用EJB,来开发企业级软件。
Struts是一种Web层开发框架,它是使用MVC模式实现的。目前,它是JavaEE领域中主流的Web层开发技术,不论使用EJB还是其他技术,一般都使用它来编写Web层。
Ant是java世界事实上的标准构建工具。它可以执行一系列的自动化任务,包括构建项目,运行软件和自动化运行单元测试等。
JUnit是XP创始人Kent Beck开发的一个单元测试工具。可以使用Ant自动化运行Junit测试。
Hibernate则是事实上的O-R映射的标准框架。它通过xml文件配置,来映射数据库表和Java程序中的POJO对象,通过同步缓存和数据库,自动更新数据库。它使我们可以像使用对象型数据库那样使用关系型数据库。
Spring框架,是一个轻量级的应用框架。Spring的目标就是提供一中贯穿始终的解决方案,将各种专用框架整合成一个连贯的整体。它可以整合Struts、Hibernate等框架,甚至简化EJB的开发和访问。
Spring的威力来源于两个方面:IOC容器和AOP框架。使用Spring框架,完全可以取代EJB的功能。
这些开源框架,可以帮助我们实现非EJB的企业级应用。
不用EJB的简单Java EE架构
对于简单的企业级应用,我们可以使用简单的技术实现。这种架构,虽然简单,但仍然很健壮。
使用Struts作为Web层开发技术,使用POJO作为业务对象,使用DAO模式封装的JDBC作为数据访问层技术。通过DAO模式,我们也实现了简单的O-R映射。
一个典型的客户用例的实现模块中,我们的类分为以下几个部分:
1,Struts的控制器模块Action,实际上,也就是“业务委派”模式的业务委派类。它们从视图中接收用户请求,然后调用业务服务类的方法实现用户的请求。
Action和Servlet一样,都是单例。也就是说,一台机器上只有一个Action类的实例,为所有的客户服务。也就是说,它不能够保存客户的状态,因为状态可以保存在HttpRequest、HttpSession、ServletContext里。
2,Struts的MVC模式中的Model—ActionForm,它用于在Action和视图之间传递数据。ActionForm中的数据,如果作为表单提交,需要是String型的。
3,领域模块的领域模型类。它是O-R映射中代表数据库表的对象。也作为参数和返回值在控制器Action类和业务服务Service类之间,以及Service类和DAO类之间传递数据。
在Action中,Action从视图中拿到的数据容器类ActionForm并不能够直接调用业务服务Service类的方法,必须要转换为领域模型对象,或者简单对象才能够作为参数调用业务服务Service类的方法。
Service类的方法返回的参数,也必须要封装到ActionForm中才能够让视图拿到显示给用户。
4,业务模块的业务服务Service接口及其实现类
Service接口中封装了Action要调用的所有业务操作。其实现类实现了这个接口。Service实现类是POJO,而不是session
bean。
Service类中要与数据库交互的操作,都委派给DAO接口的实现类。
Service实现类也应该是单例的,因为它只是提供服务,并不需要保存状态。所有的数据都可以通过方法的参数传递进来。
要实现单例,我们可以使用一个工厂类,也可以直接在Service实现类里提供单例的实现方法。这里,为了简单,我在Service实现类里直接实现了单例。
public class JDBCMessageServiceImpl implements IMessageService{
private static JDBCMessageServiceImpl instance=null;
/**
* @return JDBCMessageServiceImpl 返回这个Service类的一个实例
*/
public static JDBCMessageServiceImpl getInstance(){
if(null==JDBCMessageServiceImpl.instance){
JDBCMessageServiceImpl.instance=new JDBCMessageServiceImpl();
}
return instance;
}
……
5,领域模块的数据访问对象DAO接口及其实现类
DAO接口中封装了持久化领域模型类的所有方法。通过DAO,我们实现了内存中的领域模型类和硬盘上的数据库表之间的映射。这里,DAO的实现类是使用JDBC方式访问数据库的类。
DAO实现类一样是提供数据访问服务的服务类,也应该是单例的。我们使用和Service类一样的方式实现单例。
在DAO实现类中,我们尽量使用领域模型类作为参数和返回值。这样,就实现了领域模型类和数据库表之间的O-R映射。在DAO实现类之外,我们可以使用领域模型类来操作数据库表中的数据。
这个架构中,我们在Service模块和DAO模块中都使用了接口—实现类这种方式。对于同一个接口,可以有无数个实现类。
这里,我仅仅使用JDBC这一种方式实现DAO接口。实际上,我们可以很方便的编写一个使用iBatis,Hibernate或者JDO等其他数据访问技术的DAO实现类。
另外,我们可以使用独立于Web容器的连接池。
我们可以使用Web容器的JNDI将实现连接池的数据源发布到JNDI上,客户代码从JNDI上得到数据源的实例,调用连接池中的连接。但是,不同的Web容器发布JNDI是很不一致的,而且获得JNDI也是非常费事的。
我们可以用Apache的DBCP框架,直接编写连接池。这不需要任何Web容器的支持,可以在一般的Java程序中使用。
如果需要远程调用,我们可以在应用中集成Axis,提供Web Servics。
如果需要集群部署,我们也可以在多台服务器上部署多个Web应用程序。
一、这个架构的优点
1,不需要EJB容器,只需要Web容器即可,降低了成本。
2,不需要EJB那样累赘的部署描述符。
3,容易在不同的Web容器和应用服务器之间切换。
4,实现更加简单,业务服务Service和数据访问对象DAO都是简单的POJO类。
二、这个架构的缺点
1,这里使用了直接的单例模式来生成单例,而不是使用容器管理来实现单例,代码之间因而有一定的耦合。
2,同样,由于没有容器管理对象的实例。所以,我们必须在客户代码中手工的调用接口实现类的创建单例的方法。这样,在代码中就出现了一定的耦合。如果需要改变实现类,我们就必须修改调用实现类的客户的源代码。
如,在Action中:
ImessageService messageService=JDBCMessageServiceImpl.getInstance();
另外,在Service实现类中,也需要得到DAO实现类的单例:
public class JDBCMessageServiceImpl implements IMessageService{
private IMessageDao messageDao=null;
public JDBCMessageServiceImpl() {
/*
由于没有元数据配置,所以,必须要在构造器中直接传入类的实例。
*/
setMessageDao(JDBCMessageDaoImpl.getInstance());
}
……
3,我们必须手工编写事务,不能够使用EJB容器提供的容器管理事务CMT。EJB的CMT允许用户在配置文件中声明使用事务,而不需要手工编写事务。
4,不能够使用EJB提供的对Session Bean,Entity Bean的声明性访问控制。EJB提供的访问控制,可以控制EJB方法的访问权。
当然,这个架构一样可以通过Web Services的访问控制机制实现这一点。而且,EJB虽然提供了声明性访问控制的机制,但是由于各个EJB容器都有不同的配置方法,移植起来十分困难,所以也很少使用。
总之,这个架构是简单、健壮、功能强大的。对于小规模的企业级应用完全可以使用这种简单的架构。
对于更大规模的企业级软件,也许使用更加强大、复杂的技术框架,效率更高。比如说,使用Hibernate技术提供DAO实现类,或者更进一步,使用Spring框架来管理事务和业务对象的单例。
由于这个架构在业务服务模块Service和数据访问模块DAO中使用了接口和实现分离的方式,增加Spring、Hibernate等框架都是非常容易的。这也正反映了这个架构的健壮之处。
使用“轻量级容器”的Java EE架构
“轻量级容器”,是指提供了管理、定位业务对象的方法。一般就是指IOC反转控制容器。它实际上是提供了一个Dictionary类或相似的类,也就是名—值对的集合。在容器启动时,它生成类的实例,然后放在这个字典内,当应用程序中需要这个类的实例时,如果声明为单例,就从这个字典内按照名字取出对象。如果声明为多例模式,容器就生成一个新的实例返回给客户代码。
这样就实现了单例。不再需要像上面那样在一般的POJO类中编写实现单例的代码了。
而且,现在,调用代码中就只需要接口,不需要具体的实现类了。我们可以在配置文件中改变提供的实现类,不需要再修改源代码了。
Spring就是这样一个轻量级的容器。Java EE的JNDI实际上也可以看作是一个容器。它是一个注册表,也就是“名—值”对的集合。不同的是,它把这些对象放在了网上进行远程发布。访问JNDI对象是十分麻烦的。
另外,Spring还提供了AOP框架,这可以让它提供声明式的服务,如,EJB容器那样的声明式事务等。而且,比EJB容器更加强大,可以自己编写方面。
Spring的IOC容器和AOP框架,让它完全能够替代EJB容器。比EJB容器更简单,更强大。
当然,还存在其他的IOC容器和AOP框架,但是目前只有Spring结合了这两者。所以,这里我使用Spring来开发架构。
一、使用“轻量级容器”的Java EE架构
这个架构和前面那个不用EJB的简单Java EE架构十分类似。这里,我们给予前面的架构进行修改,将它变为使用“轻量级容器”的Java
EE架构。
Web层,我们仍然使用Struts框架。业务服务模块Service和数据访问模块DAO仍然使用POJO和接口—实现类分离的风格。在DAO的实现类中,我们为了演示Hibernate,所以增加了Hibernate的Dao实现类。当然,原来的基于JDBC的实现类一样可用。
首先,使用Spring以后,借助于Spring 的IOC容器,我们没有必要再在Service和DAO实现类中提供创建单例的方法了,也没有必要再在Service的实现类的默认构造其中创建DAO实现类的单例了。只需要空的构造器,然后再在配置文件中配置属性,将DAO实现类的实例通过set方法传进来即可。
在Struts的Action类中,也不再需要直接用Service实现类的单例方法来获得单例了,而是通过Spring的getBean(“*”)方法从Spring的注册表中得到Service实现类的单例。即使实现类改变,也不需要改变源代码,只需要修改Spring的xml配置文件即可。
Spring提供了直接集成Hibernate的方法。可以将Hibernate的SessionFactory对象也置于SpringIOC容器的管理下,生成单例。
Spring直接提供了几个类,可以在xml文件中通过配置,声明式的提供事务。不仅可以为Hibernate提供事务,也可以为我们前面架构中的JDBC实现提供声明式的事务。如果我们使用的应用服务器提供JTA事务支持,Spring也可以通过声明的方式提供JTA事务。
另外,Spring还有一个子项目,Acegi Security System for Spring。这个框架是使用Spring
AOP框架和Web容器的Filter机制,再加上本地线程变量ThreadLocal实现的一种安全机制。它在保护Web资源时,使用Web容器标准的Filter过滤器机制,在保护POJO类时,使用Spring
AOP的声明式保护,可以保护任何Spring管理的POJO的方法。这比EJB的声明式安全机制更加强大,EJB只能够保护EJB组件。
但是,Acegi的安全机制比较复杂。如果是B/S应用,仅仅需要提供对Web资源的保护,建议不要使用它。它更适合对远程调用的组件(如,Web
Services,RMI,EJB,Hessian,Burlap和Spring Http Invoker等)的方法级访问控制。
二、这个架构的优点
1,架构简单,但是功能强大,甚至比使用重量级的EJB容器的经典架构更加强大。
2,这种架构和不用EJB的简单架构一样,都可以通过部署多个应用程序,来实现集群部署。
3,与不用EJB的简单架构相比,本方案使用了一个轻量级的容器,这样做并没有增加多少复杂性,反而是减少了代码,降低了复杂性。与EJB容器相比,当然是更加容易学习和配置。
4,如果轻量级容器提供了AOP,那么就能够提供比EJB更加强大的基础设施和声明式服务。
5,这种架构不需要EJB容器。
6,Spring已经提供了很多类,让我们可以简单的访问很多Java EE服务,如JNDI,属性文件等,都可以简单的通过配置来完成。它还对一些常用的框架提供了支持,如iBatis,Hibernate等。通过Spring提供的Hibernate支持类,我们可以简单的使用Hibernate,代码量要比直接使用Hibernate少很多!
7,由于我们不使用EJB容器的服务,也很少使用Web容器提供的服务,因此,我们的应用程序可以方便的在不用的应用服务器间移植。
8,IOC可以让轻量级服务器为你“组装”对象。也就是说应用程序中不在需要编写寻找或者生成合作对象的代码了。比如说,Service实现类中已经不需要再生成DAO实现类的实例的代码了。
9,没有这些组装对象的代码,我们仍然可以使用JUnit来测试我们的应用程序。因为轻量级框架可以在任何Java程序中运行。我们可以在Junit测试类中创建和使用Spring等轻量级容器。
总结
“源代码就是设计”
1992年,Jack W.Reeves写了一篇论文《什么是软件设计》,答案是“源代码就是设计”![2]
源代码编译的过程,就是软件实现的过程,而源代码编写的过程,就是软件设计的过程。至于我们通常所说的分析软件和设计软件,只是对软件设计过程之中的更高层次的分析和设计过程。它们分析和设计的仍然是源代码这个真正的最终的设计文档。
源代码,有两类读者,一类是编译器,它将源代码编译成可以运行的软件,它对于源代码的唯一要求,就是符合语言规范,没有语法错误。另一类,就是软件的开发人员,他们需要经常地阅读和修改源代码。他们是普通的人类,只能够理解组织优良,逻辑清晰的文字。显然,源代码最重要的读者显然是这些挑剔的人类。
源代码就是设计文档,就是一篇关于软件怎样组织的说明文章。你读过几十万行字的书吗?如果你要写一本几十万行字的书,那么怎样才能够让读者能够很容易的阅读、理解、然后修改呢?
写这样一本书的要求,就是写代码的要求!显然,我们编写的代码,应该组织合理,赋予逻辑,有足够的抽象层次,能够让读者一步步地深入到细节,而不是直接把细节全部抛给读者。
章,节,段,不是很像源代码的包、类和方法吗?
健壮的软件,就是开发者容易阅读、理解,然后修改的软件;就是结构简单,逻辑清晰的软件;就是经过了层层抽象的软件,每一层都是结构简单,逻辑清晰的!
软件技术的发展,就是对源代码抽象能力的发展!
总结
本文所论述的这些编程技术和软件开发方法,不仅对于开发健壮的企业级应用有积极的意义,对开发所有类型健壮的软件也一样有意义。学习和实践这些编程技术和开发方法,将会有效的提高我们编写的软件的质量和商业价值。
致 谢
本文是在刘文予教授的悉心指导下完成的。刘教授在理论、方法和实践中都给予了我莫大的指导和帮助,使我在理论和实践上有了很大的提高,顺利完成了本篇论文。在此,谨向导师致以衷心的感谢。
我还要感谢公司里的同事。
还有James Gosling,感谢他为这个世界带来了Java这门我最钟爱的编程语言。Java改变了我的一生!
同时,还要感谢我在华中科技大学求学期间,对我给予关心,帮助的所有老师和同学。
最后,再次对所有关心、帮助我的老师、同学和同事表示衷心地感谢!
参 考 文 献
[1] Martin Fowler. 企业应用架构模式. 王怀民 周斌 译. 北京。机械工业出版社,2005年.
[2] Robert C.Martin. 敏捷软件开发:原则、模式与实践. 邓辉 译. 北京。清华大学出版社, 2003年.
[3] Erich Gamma, Richard Helm, Ralph Johnson, et al. 设计模式:可复用面向对象软件的基础.
李英军,马晓星,蔡敏等 译. 北京。机械工业出版社, 2005年.
[4] Rod Johnson. J2EE设计开发编程指南. 魏海萍 译. 北京。 电子工业出版社, 2003年.
[5] Martin Fowler. 重构—改善既有代码的设计. 侯捷,熊节 译. 北京。 中国电力出版社, 2003年.
[6] Kent Beck. 解析极限编程—拥抱变化. 唐东铭 译. 北京。人民邮电出版社, 2002年.
[7] Rod Johnson, Juergen Hoeller. Expert One-on-One J2EE Development
without EJB中文版. JavaEye 译. 北京。 电子工业出版社, 2005年.
[8] Iver Jacoboson, Pan-Wei Ng. AOSD中文版—基于用例的面向方面软件开发. 徐锋 译. 北京。
电子工业出版社, 2005年. .77-85
[9] Deepak Alur, John Crupi, Dan Malks. J2EE核心模式、第二版.
北京。 机械工业出版社, 2005年. .218-227
[10] Mark Cade, Simon Roberts. J2EE架构师认证指南. 武欣,罗云峰,刘侃 译. 北京。 机械工业出版社,
2004年. .15-17
|