要进行应用软件的设计,分层是非常重要的思想,掌握好分层的思想,设计出的软件是可以令人赏心悦目的。由于这一章的重要性和特殊性,本章的内容分为上下两节,并不采取模式描述语言的方式。
分层只是将系统进行有效组织的方式。 本章特别针对于企业应用进行讨论,但其中大部分的内容都可以应用在其它的系统中,或为其它的系统所参考。
在企业应用中,有两个非常重要的概念:业务逻辑和持久性。可以说,企业应用是围绕着业务逻辑进行开展的。例如报销、下订单、货品入库等都是业务逻辑。从业务逻辑的底层实现来看,业务逻辑其实是对业务实体进行组织的过程。这一点对于面向对象的系统才成立,因为在面向对象的系统中,识别业务实体,并制定业务实体的行为是非常基础的工作,而不同的业务实体的组合就形成了业务逻辑。
还有另一个重要的概念是持久性。企业应用中大部分的数据都是需要可持久化的。因此,基础组织支持持久性就显得非常的重要。目前最为通行的支持持久性的机制是数据库,尤其是关系性数据库-RDBMS。
除此之外,在企业应用中还有其它的重要概念,例如人机交互。
为了能够更有效的对企业中的各种逻辑进行组织,我们使用层技术来实现企业应用。层技术在计算机领域中有着悠久的历史,计算机的实现中就引用了分层的概念。TCP/IP的七层协议栈也是典型的分层的概念。分层的优势在于:
上层的逻辑不需要了解所有的底层逻辑,它只需要了解和它邻接的那一层的细节。我们知道TCP/IP协议栈就是通过不同的层对数据进行层层封包的,不同层间的耦合度明显降低。通过严格的区分层次,大大降低了层间的耦合度。
某一层次的下级层可以有不同的实现。例如同样的编程语言可以在不同的操作系统和不同的机器中运行。
和第三条类似的,同一个层次可以支持不同的上级层。TCP协议可以支持FTP、HTTP等应用层协议。
综合上面的考虑,我们把企业应用分为多个层次。企业应用到底应该分为几种层次,目前还没有统一的意见。
在前些年的软件开发中,两层结构占有很重要的位置。例如在银行中应用很广的大型主机/终端方式,以及Client/Server方式。两层的体系结构一直到现在还广泛存在,但是两层结构却有着很多的缺点,例如客户端的维护成本高、难以实现分布式处理。随着在两层结构的终端用户和后端服务间加入更多的层次,多层的结构出现了。
经典的三层理论将应用划分为三个层次:
表示层(Presentation Layer),用于处理人机交互。目前最主流的两种表示层是Windows格式和WebBrowser格式。它主要的责任是处理用户请求,例如鼠标点击、输入、HTTP请求等。
领域逻辑层(Domain Logic Layer),模拟了企业中的实际活动,也可以认为是企业活动的模型。
数据层(Data source Layer),处理数据库、消息系统、事务系统。
在实际的应用中,三层结构有一些变化。例如,在Windows的.NET系统中,把应用分为三个层次:表示层(Presentation Layer)、业务层(Business Layer)、数据访问层(Data Aclearcase/" target="_blank" >ccess Layer),分别对应于经典的三层理论中的三个层次。值得一提的是,.NET系统中表示层可以直接访问数据访问层,即记录集技术。在ADO.NET中,这项技术已经非常成熟,并通过表示层中的某些数据感知组件,实现非常友好的功能。这种越层访问的技术通常被认为是不被允许的,因为它可能会破坏层之间的依赖关系。而在Windows平台中,严格遵守准则就意味着需要大量额外的工作量。因此,我们看到准则也不是一成不变的。
在J2EE的环境中,三层结构演变为五层的结构。在表示层这里,J2EE将其分为运行在客户机上的用户层(Client Layer),以及运行在服务端上的Web层(Presentation Layer)。这样做的主要理由是Web Server已经成为J2EE中非常核心的技术,例如JSP和Java Servlet都和它有关系。Web层为用户层提供表示逻辑,并对用户的请求产生回应。
业务层(Business Layer)并没有发生变化,仍然是处理应用核心逻辑之处。而数据层则被划分为两个层次:集成层(Integration Layer)和资源层(Resource Layer)。其中,资源层并非J2EE所关心的内容,它可能是数据库或是其它的老系统,集成层是重要的层次,包括事务处理,数据库映射系统。
实例
这一章的的组织方式和之前的模式有一些差别。我们先从一个例子来体会架构设计中分层的重要性。
上图是一个业务处理系统的软件架构图。在上图中,我们把软件分为四个层次。这种层次结构类似于我们前面所谈的J2EE的分层。但是和J2EE不同的是,缺少了一个Web Server层,这是因为目前我们还没有任何对Web Server的需要。
在资源层上,我们有三种资源:数据库、平台服务、UI。数据库是企业应用的基础,而这里的平台服务指的是操作系统系统或第三方软件提供的事务管理器的功能。值得注意的是,这里的事务管理器指的并不是数据库内部支持的事务,而是指不同的业务实体间事务处理,这对于企业应用有着很重要的意义。因为对于一个企业应用来说,常常需要处理跨模块、跨软件、甚至跨平台的会话(Session),这时候,单纯的数据库支持的事务往往就难以胜任了。这方面的例子包括微软的MTS和Oracle的DBLink。当然,如果说,在你处理的系统中,可以使用数据库事务来处理大部分的会话的话,那就可以避免考虑这方面的设计了。除了典型的事务管理器,平台还能够提供其它的服务。对于大部分的企业应用来说,都是集成了多个的平台服务,平台服务对架构的设计至关重要。但是从分层的角度上考虑,上层的设计应该尽可能的和平台无关。和使用平台服务类似的,一般来说,企业应用都不会从头设计界面,大部分情况下都会使用现有的的UI资源。比如Window平台的MFC中的界面部分。因此,我们把被使用的UI资源也归到资源层这个层次上。
资源层的上一层是集成层。集成层主要完成两项工作,第一项是使用资源层的平台服务,完成企业应用中的事务管理。有些事务处理机制已经提供了比较好封装机制,直接使用资源层的平台服务就可以了。但是对于大多数的应用来说,平台提供的服务往往是比较简单的,这时候集成层就派上大用场了。第二项是对上一层的对象提供持久性机制。可以说,这是一组起到过渡作用的对象。它实际上使用的是资源层的数据库功能,并为上一层的对象提供服务。这样,上一层的业务对象就不需要直接同数据库打交道。对于那些底层使用关系型数据库,编程中使用面向对象技术的系统来说,目前比较常见的处理持久性的做法是对象/关系映射(OR Mapping)。
在这个层次上,我们可以做一些扩展,来分析层的作用。假设我们的系统需要处理多个数据库,而不同数据库之间的处理方式有一定的差异。这时候,层的作用就显示出来了。我们在集成层中支持对多个数据库的处理,但对集成层以上的层次提供统一的接口。对于业务层来说,它不知道,也不需要知道数据库的差别。目前我们自己开发了集成层中的持久类,但是随着功能的扩展,原有的类无法再支持新增加的功能了,新的解决方案是购买商用程序。为了尽可能的保持对业务层次的影响,我们仍然使用原有的接口,但是具体的实现已经不同了,新的代码是针对新的商业程序来实现的。而对业务层来说,最理想的状况是不需要任何的改变。当然现实中不太可能出现如此美好的情况,但可以肯定的一点是,引入层次比不引入层次要好的多。
以上列举的两个例子都是很好的解决了耦合度的问题。关于分层中的耦合度的问题,我们在下面还会讨论。
业务层的设计比较简单,暂时只是把它实现为一组的业务类。类似的,表示层的设计也没有做更多的处理。表示层的类是继承自资源层的。这是一种处理的方法,当然,也可以是使用关系,这和具体的实现环境和设计人员的偏好都有关系,并不是唯一的做法。在对软件的大致架构有了一个初步了解之后,我们需要进一步挖掘需求,来细化我们的设计。在前面的设计中,我们对业务层的设计过于粗糙了。在我们的应用中,还存在一个旧系统,这个系统中实现了应用规则,从应用的角度来看,这些规则目前仍然在使用,但新的系统中会加入新的规则。在新系统启用后,旧的系统中的规则处理仍然需要发挥它的作用,因此不能够简单的把所有的规则转移到新系统中。(有时候我们是为了节省成本而不在新系统中实现旧系统的逻辑)。我们第二步的架构设计的细化过程中将会加入对新的要求的支持。
在细化业务层的过程中,我们仍然使用层技术。这时候,我们把原先的业务层划分为两个子层。对于大多数的企业应用来说,业务层往往是最复杂的。企业对象之间存在着错综复杂的联系,企业的流程则需要用到这些看似独立的企业对象。我们希望在业务层中引入新的机制,来达到组织业务类的目的。业务层的组织需要依赖于具体的应用环境,金融行业的应用和制造行业的应用就有着巨大的差距。这里,我们从一般性的设计思考的角度出发来看待我们的设计:
首先,我们看到,业务层被重新组织为两个层次。增加层次的主要考虑是设计的重用性。从我们前面对层的认识,我们知道。较高的层次可以重用较低的层次。因此,我们把业务层中的一部分类归入较低的层次,以供较高的层次使用。降低类层次的主要思路是分解行为、识别共同行为、抽取共性。
在Martin Fowler的分析模式中提供了一种将操作层和知识层分离的处理方法:
Action、Accountability、Party属于较高层次的操作层(Operational Layer),这一层次的特点是针对于特定的应用。但是观察Accountability和Party,有很多相似的职责。也就是说,我们对于知识的处理并不合适。因此,Martin Fowler提出了新的层次――属于较低层次的知识层(Knowledge Layer)。操作层中可重用的知识被整理到知识层。从而实现对操作层的共性抽取。注意到虽然图中的层次(Level)的概念和层(Layer)有所差别,但是思路是基本一致的。
另一种分层方法是利用继承:
该图中也是来自于分析模式一书。不同的部门有着差异点和共性,将共性提取到父类中是继承的基本概念。这时候我们可以把父类看作是较低的层次,而把子类看作是较高的层次。对于一组层次结构很深的类来说,也可以从某一个水平线上分离层次。
最后一种方法需要分解并抽象对象的行为。C++的STL中为不同的数据类型和不同的容器实现了Iterator的功能。它的实现是典型的降低层次的行为。我们不考虑它对不同数据类型的支持,因为它使用了比较独特的模板(Template)技术,只考虑它对不同的容器的实现。首先是对共性的分析,不论是数组(Array)还是向量(Vector),要执行遍历操作,都需要知道三个条件:容器的起始点、容器的长度、匹配值。这就是一种抽象。通过这种方式,就可以统一不同容器的接口。
以上我们讨论了细分层次的好处和实现策略。下面我们回到前例中,继续讨论层中的各个部件。
业务实体指的是企业中的一些单独的对象。例如订单、客户、供应商等。由于业务实体可以被很多的业务流程(业务会话)所使用,因此我们把业务实体放在业务实体层中。企业中的业务实体大多是需要持久性的。因此在我们的设计中,业务实体将持久性的职责委托给下一个层次中的持久性包,而不是直接调用数据库方法。另一种常用的方法是使用继承――业务实体继承自持久类。EJB中的Entity Bean就是典型的业务实体,它可以支持自动的持久性机制。
在我们的系统中,规则是一个尴尬的存在。部分的规则处于旧系统中,并使用旧系统的持久性机制。而新系统中有需要支持新的规则。对于上层的用户来说,并不需要知道新旧系统的规则。如果我们在使用规则时做一个新旧规则的判断并调用不同的系统函数,那就显得太傻了。我们的设计对上层而言应该是透明的。
所以我们总共设计了三个包来实现规则的处理。包装器包装了旧系统中的规则,这样做处于几点考虑:首先,旧系统是面向过程的,因此我们需要用包装器将函数(或过程)封装到类中。其次,对旧系统的使用需要额外的程序(或平台服务)的支持,因此需要单独的类来处理。最后,我们可以使用Adapter模式[GOF 94]将新旧系统不同的接口给统一起来,以使他们能够一起工作。新的规则类实现了新的规则,较好的做法是定义一个虚协议,具体的表现形式可以是虚基类或接口。然后由其子类实现虚协议。再结合前面介绍的把旧规则的接口转换为新的接口的方法,就能够统一规则接口。从图中我们看到,业务实体需要使用规则,通过统一的接口,业务实体就能够透明的使用新旧两种规则。
在定义了规则和标准的规则接口之后,上一层的规则控制器就可以通过调用规则,来实现不同规则组合。因此这个规则控制器就类似于应用规则。在业务交易处理中需要调用规则控制器来实现部分的功能。
请注意,这里讨论的思路虽然非常的简单、清晰,但是现实中的处理却没有这么容易。因为为新旧规则制定统一的接口实在是太难了。要能够使接口在未来也相对的稳定更是难上加难。而在应用已经部署之后,对已发布的接口的任何一个小改动都意味着很高的成本。在这个问题上,并没有什么好的策略,经验是最重要的。在实际中的一个比较实用的方法是为接口增加一些额外的参数。即便可能这个参数当前并没有使用到,但是如果为了有可能使用的话,那还是加上吧。举个例子,对于商业应用软件而言,数据库,或者说是关系数据库一定是不可缺少的部分。大量的操作都需要和数据库结合起来,因此,可以考虑在方法中加入一个数据库连接字符串的参数。虽然这对于很多方法而言是没什么必要的,但是为将来的实践提供了一个可扩展的空间。国内的某个知名的ERP软件的设计就采用了这种思路。
本章的主要精力都放在实例的研究上,在有了一个基本的概念之后,我们再回来谈谈分层的一些原则和需要注意的问题。
(待续)
作者简介:
林星,辰讯软件工作室项目管理组资深项目经理,有多年项目实施经验。辰讯软件工作室致力于先进软件思想、软件技术的应用,主要的研究方向在于软件过程思想、Linux集群技术、OO技术和软件工厂模式。您可以通过电子邮件 iamlinx@21cn.com 和他联系。