当代第一IT诗人 ——代腾飞

导航

架构设计中的方法学(五)

在定义了架构愿景之后,团队中的所有人员应该对待开发的软件有一定的了解了。但是,面对一个庞大的软件系统,接下来要做些什么呢?分而治之的思想是计算机领域非常重要的思想,因此我们也从这里开始入手。

  要进行应用软件的设计,分层是非常重要的思想,掌握好分层的思想,设计出的软件是可以令人赏心悦目的。由于这一章的重要性和特殊性,本章的内容分为上下两节,并不采取模式描述语言的方式。

  分层只是将系统进行有效组织的方式。

  本章特别针对于企业应用进行讨论,但其中大部分的内容都可以应用在其它的系统中,或为其它的系统所参考。

  在企业应用中,有两个非常重要的概念:业务逻辑和持久性。可以说,企业应用是围绕着业务逻辑进行开展的。例如报销、下订单、货品入库等都是业务逻辑。从业务逻辑的底层实现来看,业务逻辑其实是对业务实体进行组织的过程。这一点对于面向对象的系统才成立,因为在面向对象的系统中,识别业务实体,并制定业务实体的行为是非常基础的工作,而不同的业务实体的组合就形成了业务逻辑。

  还有另一个重要的概念是持久性。企业应用中大部分的数据都是需要可持久化的。因此,基础组织支持持久性就显得非常的重要。目前最为通行的支持持久性的机制是数据库,尤其是关系性数据库-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 Access 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所关心的内容,它可能是数据库或是其它的老系统,集成层是重要的层次,包括事务处理,数据库映射系统。

  实例

  这一章的的组织方式和之前的模式有一些差别。我们先从一个例子来体会架构设计中分层的重要性。
http://developer.ccidnet.com/pub/attachment/2003/6/196808.gif

 上图是一个业务处理系统的软件架构图。在上图中,我们把软件分为四个层次。这种层次结构类似于我们前面所谈的J2EE的分层。但是和J2EE不同的是,缺少了一个Web Server层,这是因为目前我们还没有任何对Web Server的需要。

  在资源层上,我们有三种资源:数据库、平台服务、UI。数据库是企业应用的基础,而这里的平台服务指的是操作系统系统或第三方软件提供的事务管理器的功能。值得注意的是,这里的事务管理器指的并不是数据库内部支持的事务,而是指不同的业务实体间事务处理,这对于企业应用有着很重要的意义。因为对于一个企业应用来说,常常需要处理跨模块、跨软件、甚至跨平台的会话(Session),这时候,单纯的数据库支持的事务往往就难以胜任了。这方面的例子包括微软的MTS和Oracle的DBLink。当然,如果说,在你处理的系统中,可以使用数据库事务来处理大部分的会话的话,那就可以避免考虑这方面的设计了。除了典型的事务管理器,平台还能够提供其它的服务。对于大部分的企业应用来说,都是集成了多个的平台服务,平台服务对架构的设计至关重要。但是从分层的角度上考虑,上层的设计应该尽可能的和平台无关。和使用平台服务类似的,一般来说,企业应用都不会从头设计界面,大部分情况下都会使用现有的的UI资源。比如Window平台的MFC中的界面部分。因此,我们把被使用的UI资源也归到资源层这个层次上。

  资源层的上一层是集成层。集成层主要完成两项工作,第一项是使用资源层的平台服务,完成企业应用中的事务管理。有些事务处理机制已经提供了比较好封装机制,直接使用资源层的平台服务就可以了。但是对于大多数的应用来说,平台提供的服务往往是比较简单的,这时候集成层就派上大用场了。第二项是对上一层的对象提供持久性机制。可以说,这是一组起到过渡作用的对象。它实际上使用的是资源层的数据库功能,并为上一层的对象提供服务。这样,上一层的业务对象就不需要直接同数据库打交道。对于那些底层使用关系型数据库,编程中使用面向对象技术的系统来说,目前比较常见的处理持久性的做法是对象/关系映射(OR Mapping)。

  在这个层次上,我们可以做一些扩展,来分析层的作用。假设我们的系统需要处理多个数据库,而不同数据库之间的处理方式有一定的差异。这时候,层的作用就显示出来了。我们在集成层中支持对多个数据库的处理,但对集成层以上的层次提供统一的接口。对于业务层来说,它不知道,也不需要知道数据库的差别。目前我们自己开发了集成层中的持久类,但是随着功能的扩展,原有的类无法再支持新增加的功能了,新的解决方案是购买商用程序。为了尽可能的保持对业务层次的影响,我们仍然使用原有的接口,但是具体的实现已经不同了,新的代码是针对新的商业程序来实现的。而对业务层来说,最理想的状况是不需要任何的改变。当然现实中不太可能出现如此美好的情况,但可以肯定的一点是,引入层次比不引入层次要好的多。

  以上列举的两个例子都是很好的解决了耦合度的问题。关于分层中的耦合度的问题,我们在下面还会讨论。

  业务层的设计比较简单,暂时只是把它实现为一组的业务类。类似的,表示层的设计也没有做更多的处理。表示层的类是继承自资源层的。这是一种处理的方法,当然,也可以是使用关系,这和具体的实现环境和设计人员的偏好都有关系,并不是唯一的做法。在对软件的大致架构有了一个初步了解之后,我们需要进一步挖掘需求,来细化我们的设计。在前面的设计中,我们对业务层的设计过于粗糙了。在我们的应用中,还存在一个旧系统,这个系统中实现了应用规则,从应用的角度来看,这些规则目前仍然在使用,但新的系统中会加入新的规则。在新系统启用后,旧的系统中的规则处理仍然需要发挥它的作用,因此不能够简单的把所有的规则转移到新系统中。(有时候我们是为了节省成本而不在新系统中实现旧系统的逻辑)。我们第二步的架构设计的细化过程中将会加入对新的要求的支持。

  在细化业务层的过程中,我们仍然使用层技术。这时候,我们把原先的业务层划分为两个子层。对于大多数的企业应用来说,业务层往往是最复杂的。企业对象之间存在着错综复杂的联系,企业的流程则需要用到这些看似独立的企业对象。我们希望在业务层中引入新的机制,来达到组织业务类的目的。业务层的组织需要依赖于具体的应用环境,金融行业的应用和制造行业的应用就有着巨大的差距。这里,我们从一般性的设计思考的角度出发来看待我们的设计:

http://developer.ccidnet.com/pub/attachment/2003/6/196809.gif

 首先,我们看到,业务层被重新组织为两个层次。增加层次的主要考虑是设计的重用性。从我们前面对层的认识,我们知道。较高的层次可以重用较低的层次。因此,我们把业务层中的一部分类归入较低的层次,以供较高的层次使用。降低类层次的主要思路是分解行为、识别共同行为、抽取共性。

  在Martin Fowler的分析模式中提供了一种将操作层和知识层分离的处理方法:
http://developer.ccidnet.com/pub/attachment/2003/6/196810.gif

 Action、Accountability、Party属于较高层次的操作层(Operational Layer),这一层次的特点是针对于特定的应用。但是观察Accountability和Party,有很多相似的职责。也就是说,我们对于知识的处理并不合适。因此,Martin Fowler提出了新的层次――属于较低层次的知识层(Knowledge Layer)。操作层中可重用的知识被整理到知识层。从而实现对操作层的共性抽取。注意到虽然图中的层次(Level)的概念和层(Layer)有所差别,但是思路是基本一致的。

  另一种分层方法是利用继承:
http://developer.ccidnet.com/pub/attachment/2003/6/196876.gif

 该图中也是来自于分析模式一书。不同的部门有着差异点和共性,将共性提取到父类中是继承的基本概念。这时候我们可以把父类看作是较低的层次,而把子类看作是较高的层次。对于一组层次结构很深的类来说,也可以从某一个水平线上分离层次。

  最后一种方法需要分解并抽象对象的行为。C++的STL中为不同的数据类型和不同的容器实现了Iterator的功能。它的实现是典型的降低层次的行为。我们不考虑它对不同数据类型的支持,因为它使用了比较独特的模板(Template)技术,只考虑它对不同的容器的实现。首先是对共性的分析,不论是数组(Array)还是向量(Vector),要执行遍历操作,都需要知道三个条件:容器的起始点、容器的长度、匹配值。这就是一种抽象。通过这种方式,就可以统一不同容器的接口。

  以上我们讨论了细分层次的好处和实现策略。下面我们回到前例中,继续讨论层中的各个部件。

  业务实体指的是企业中的一些单独的对象。例如订单、客户、供应商等。由于业务实体可以被很多的业务流程(业务会话)所使用,因此我们把业务实体放在业务实体层中。企业中的业务实体大多是需要持久性的。因此在我们的设计中,业务实体将持久性的职责委托给下一个层次中的持久性包,而不是直接调用数据库方法。另一种常用的方法是使用继承――业务实体继承自持久类。EJB中的Entity Bean就是典型的业务实体,它可以支持自动的持久性机制。

  在我们的系统中,规则是一个尴尬的存在。部分的规则处于旧系统中,并使用旧系统的持久性机制。而新系统中有需要支持新的规则。对于上层的用户来说,并不需要知道新旧系统的规则。如果我们在使用规则时做一个新旧规则的判断并调用不同的系统函数,那就显得太傻了。我们的设计对上层而言应该是透明的。

  所以我们总共设计了三个包来实现规则的处理。包装器包装了旧系统中的规则,这样做处于几点考虑:首先,旧系统是面向过程的,因此我们需要用包装器将函数(或过程)封装到类中。其次,对旧系统的使用需要额外的程序(或平台服务)的支持,因此需要单独的类来处理。最后,我们可以使用Adapter模式[GOF 94]将新旧系统不同的接口给统一起来,以使他们能够一起工作。新的规则类实现了新的规则,较好的做法是定义一个虚协议,具体的表现形式可以是虚基类或接口。然后由其子类实现虚协议。再结合前面介绍的把旧规则的接口转换为新的接口的方法,就能够统一规则接口。从图中我们看到,业务实体需要使用规则,通过统一的接口,业务实体就能够透明的使用新旧两种规则。

  在定义了规则和标准的规则接口之后,上一层的规则控制器就可以通过调用规则,来实现不同规则组合。因此这个规则控制器就类似于应用规则。在业务交易处理中需要调用规则控制器来实现部分的功能。

  请注意,这里讨论的思路虽然非常的简单、清晰,但是现实中的处理却没有这么容易。因为为新旧规则制定统一的接口实在是太难了。要能够使接口在未来也相对的稳定更是难上加难。而在应用已经部署之后,对已发布的接口的任何一个小改动都意味着很高的成本。在这个问题上,并没有什么好的策略,经验是最重要的。在实际中的一个比较实用的方法是为接口增加一些额外的参数。即便可能这个参数当前并没有使用到,但是如果为了有可能使用的话,那还是加上吧。举个例子,对于商业应用软件而言,数据库,或者说是关系数据库一定是不可缺少的部分。大量的操作都需要和数据库结合起来,因此,可以考虑在方法中加入一个数据库连接字符串的参数。虽然这对于很多方法而言是没什么必要的,但是为将来的实践提供了一个可扩展的空间。国内的某个知名的ERP软件的设计就采用了这种思路。

  本章的主要精力都放在实例的研究上,在有了一个基本的概念之后,我们再回来谈谈分层的一些原则和需要注意的问题。

九、-- 分层 (下)



  上篇我们用了大量的篇幅来观察了一个实际的例子,相信大家已经对分层有了一个比较具体的概念了。在这一篇中我们就对分层在实践中可能会遇到的问题做一个讨论。分层在架构设计中是一种非常常见的,但是又很不容易用好的技术。因此我们这里花了很大的气力来讨论它。

  由于这是一篇介绍软件设计技术的文章,为了尽可能让更多的人理解,本应该尽可能不涉及到过于具体的技术或平台。但是这个目标可能很难实现,因为软件设计是没办法脱离具体的实现技术的。因此本文能够做到的是尽可能的不涉及具体的编码细节。

  何时使用分层技术?

  分层技术实际上是把技术复杂化了。和以往简单的CS结构的系统不同,分层往往需要使用特定的技术平台来实现。当然,不使用这些技术平台也是可能的,但是效果可能就没有那么好了。支持分层技术的平台有很多,包括目前主流的J2EE和.NET。甚至在不同厂商的开发平台上,要求也不一样。使用分层技术实现的多层架构,成本要比普通的CS架构高得多。

  这就产生了一个非常现实的问题-并不是所有的软件都适合采用分层技术的。一般来说,小型的软件使用分层并没有太大的意义,因为分层导致的成本超过它所能带来的好处。在一般的CS结构中,可以把界面控制、逻辑处理和数据库访问都放在一块儿。这种设计方式在纯粹的多层主义者看来简直就是十恶不赦。但是对于小型的软件而言,这并不是什么大不了的事情。因为从表示层到数据层的整套功能都被囊括在一个功能块中,同样能够实现较好的封装。而且,如果结构设计的足够好,也能够避免表示层、业务层和数据层之间出现过高的耦合度。因此,除非确实需要,不然没有必要使用分层技术。

  尤其在处理一些特殊的项目时,严格的区分三层结构并不理想。比如在快速开发windows界面的应用时,往往会用到一些对数据库敏感的控件,这种处理方法跨越了三个层次,但是却很实用,成本也比较低。又比如一些框架,给出了从界面层到数据库的综合的解决方案,和windows的应用类似,严格的三层技术也不适用于这种情况。

  如何使用分层技术?

  从某种意义上来看,层其实是一个粗粒度的组件。就像我们使用组件技术是为了对系统进行一种划分一样,层的一个很大的作用也是如此。其目的是为了系统更容易被理解,不同的部分能够被较容易的替换。

  使用分层技术的依据是软件开发人员的实际需要。如果你是在使用某些优秀的面向对象的软件开发平台的话,那它们一般都会建议(或是强制)你使用某一种分层机制。这是你采用分层技术的一大参考。

  对于大多数有一定经验的软件团队而言,一般都会积累一些软件开发经验。其中包含了很多在某些特定的领域中使用的基础的类或组件。这些元素构成了一个系统的通用层次。这个层次也是分层时需要考虑的。例如一些应用软件中使用的一些通用的Currency对象或是Organization对象。分析模式一书对此类的对象进行了充分细致的阐述。这个层次一般被称为跨领域层(cross-domain layer),或称为工具层(utility layer)。

  目前的很多软件都采用了数据库映射技术。数据库映射层对于企业应用系统非常的重要,因此也需要纳入考虑之列。数据库映射技术用起来简单,但是要实现可不容易。如果不是非常有必要,尽可能使用现成的框架,或是采用其中部分的设计思路。试图构建一个大而全的映射层次的代价是非常高昂的,认识不到这一点会带来很大的麻烦。数据库映射技术的知识,我们在下文中还有专门的篇幅来讨论。

  如何存放数据(状态)?

  在学习EJB的过程中,最先要理解的一定是有状态和无状态的概念。可以说,整个概念是多层体系的核心。为什么这么说呢?这里的状态指的是类的状态,例如类的属性、变量等。由于状态的不同,类也表现出差异来。而对于多层结构的软件,创建和销毁一个类的开销是很大的,如果该软件支持分布式的话尤为如此。所以如果系统的不同层次间进行频繁的调用-创建一个类,再销毁一个类。这种做法是非常消耗资源的。在应用系统的设计中,一般不单独使用COM,就是这个原因。所以我们很自然的想到了一种经典的设计-缓冲池。把对象存放在缓冲池中,当需要的时候从池中取出一个,当不需要的时候再把对象放入池中。这种设计思路能够大幅度的提高效率。但是这对能够放在池中的对象也提出了苛刻的要求-所有的对象必须是无差异的,也就是无状态的。只有这样才能够实现缓冲池。

  一般来说,对象缓冲池的技术是用在中间的业务层上的。既然中间业务层上不能够保留有状态,那就出现了一个状态转移的问题。这里有两种的选择,一种是前移,把状态移到用户端,最典型的是使用cookie。这种选择一般是由于状态和用户端有关,不需要长时间保存。另一种选择是后移,把状态移到数据层,由数据库来实现持久性状态,当需要时才把状态提交给业务层。这种方式是企业应用软件中采用最多的,但是也增大了数据库的负担。

  处理好接口

  由于使用了分层技术,因此原先那种在CS结构中类之间存在复杂关系就有必要重新评估了。一般层间的耦合度不宜过大。因此需要慎重的设计层之间的类调用方式。一些分布式软件体系(例如J2EE)对层之间的调用方式以接口的形式给出了要求。同时,不同层之间仅仅知道目标层的接口,而不知道目标层的具体实现。EJB的home接口和remote接口就是这样。在COM+体系中,也需要在设计类的同时,把接口公布出来,以供客户方使用。

  在设计层间的接口时,除了考虑开发平台的约束之外,还有一点是开发人员必须考虑的。那就是业务需要。业务层中往往有非常多的对象和方法,它们之间的关系也非常的负责,但对于其它的层次来说,它并不关心这些细节。因此业务层公布的接口必须要简单,而且和实现无关。因此,可以使用设计模式的Facade模式来简化层间的接口。这种做法非常有效,EJB中的SessionBean和EntityBean区分就含有这种设计思路。

  同样的,不同层之间的数据传递也存在问题。如果不同层的物理节点在一起还好办,如果不在一起,那就需要使用到分布式技术了。因为不同机器的内存地址编码是不同的,如果接口之间采用对象引用的方式,那一定会出现问题。因此会将对象打包成字符串,发送到目标机器后再还原为对象。所有的分布式平台都提供了对这种技术的支持,这里就不多说了。但是这种实现技术会对我们的设计思路产生影响,少量的数据直接使用字符串来传递,数据量大的话,就需要使用封装了数据的对象。这类对象的设计需要非常的小心。在设计过程中可以参照开发平台提供的一些标准做法。同样的,数据的请求的频率也是难点之一。过于频繁的操作来自后端的数据会加大系统的开销。因此,在设计调用方法时同样需要结合实际应用来考虑。

  兼顾效率

  一般来说,纯粹的面向对象设计者设计出的软件都会比较完美。但是需要付出一定的代价。在一些大的软件平台上编程的时候,往往需要利用到平台的一些机制。最典型的就是平台的事务机制(最典型的包括J2EE平台的JTS,以及COM+平台的MTS),但是事务机制的实现往往需要平台大量对象的支撑。这种情况下,创建一个支持事务的对象的开销是很大的。处理这种问题有一种变通的办法,就是仅仅对需要事务支撑的对象提供事务支持。这就意味着,一个单独的业务实体类,可能需要根据是否支持事务分为两种类:对该业务实体的select方法不需要事务的支持,只有update和delete方法才需要有事务的支持。这是不符合纯面向对象设计者的观点的。但是这种做法却可以获得比较优秀的效率。


http://developer.ccidnet.com/pub/attachment/2003/6/196887.gif

上图 将单个的业务实体分为不同的实现

  应该承认,这种提高效率的做法加大了复杂度。因为对于客户端来说,它们并不关心具体的实现技术。要求客户端在某一种情况下调用这个类,在其它情况下又调用另一个类,这种做法既不符合面向对象的设计思路,也增大了层间耦合度及复杂性。因此,我们可以考虑使用接口或是外观类(参见设计模式一书中的facade模式),把具体的实现封装起来,而只把用户关心的部分提供给用户。这方面的技巧我们在下面的章节中还会提到。

  以迭代的方式进行分层

  软件设计中的迭代做法同样可以适用于分层。根据自己的经验,在一开始就定义好所有的层次是很难的。除非有着非常丰富的经验,都则实现和原先的设计总有或大或小的差距。因此调整势在必行。每一次的迭代都能够对分层技术进行改进,并为后一个项目积累了经验。

  这里的分层迭代不可以过于频繁,每一次的迭代都是对架构的重大修改,都是需要投入人力的,而且会影响到软件开发的进度。但是成功的迭代的效果是非常明显的,能够在接下来的开发周期中起到稳定架构,减少代码量,提升软件质量的功效。注意,不要让新潮技术成为分层迭代的推动力。这是开发人员都常犯的毛病,这并不是什么缺点,只能称为一种职业病吧。分层迭代的推动力应该源自于需求的演进以及现有架构的不稳定已经妨碍了软件进一步的开发。因此这需要团队中的技术主管对技术有着非常好的把握。

  重构能够对迭代有所帮助。嗅出代码中隐藏的坏味道并加以改进。应该说,迭代是一种比较激烈的做法,更好的做法是在开发中不断的对架构、层次进行调整。但这对团队、技术、方法、过程都有着很高的要求。因此迭代仍然是一种主要的改进手段。

  层内的细分

  分层的思路还可以适用于层的内部。层内的细分并没有固定的方式,其驱动因素往往是出于封装性和重用的考虑。例如,在EJB体系中的业务层中,实体Bean负责实现业务对象,因此一个应用往往拥有大量的实体Bean。而用户端并不需要了解每一个的实体Bean,对它们来说,只要能够完全一些业务逻辑就可以了,但完成这些业务逻辑则需要和多个实体Bean打交道。因此EJB提供了会话Bean,来负责把实体Bean封装起来,用户只知道会话Bean,不知道实体Bean的存在。这样既保证了实体Bean的重用性,又很好的实现了封装。

  面向接口编程

  在前面的章节中,我们提到一个接口设计的例子。为什么我们提倡接口的设计呢?Martin Fowler在他的分析模式一书中指出,分析问题应该站在概念的层次上,而不是站在实现的层次上。什么叫做概念的层次呢?简单的说就是分析对象该做什么,而不是分析对象怎么做。前者属于分析的阶段,后者属于设计甚至是实现的阶段。在需求工程中有一种称为CRC卡片的玩艺儿,是用来分析类的职责和关系的,其实那种方法就是从概念层次上进行面向对象设计。因此,如果要从概念层次上进行分析,这就要求你从领域专家的角度来看待程序是如何表示现实世界中的概念的。下面的这句话有些拗口,从实现的角度上来说,概念层次对应于合同,合同的实现形式包括接口和基类。简单的说吧,在概念层次上进行分析就是设计出接口(或是基类),而不用关心具体的接口实现(实现推迟到子类再实现)。结合上面的论述,我们也可以这样推断,接口应该是要符合现实世界的观念的。

  在Martin Fowler的另一篇著作中提到了这样一个例子,非常好的解释了接口编程的思路:

interface Person { public String name(); public void name(String newName); public Money salary (); public void salary (Money newSalary); public Money payAmount (); public void makeManager (); } interface Engineer extends Person{ public void numberOfPatents (int value); public int numberOfPatents (); } interface Salesman extends Person{ public void numberOfSales (int numberOfSales); public int numberOfSales (); } interface Manager extends Person{ public void budget (Money value); public Money budget (); }



  可以看到,为了表示现实世界中人(这里其实指的是员工的概念)、工程师、销售员、经理的概念,代码根据人的自然观点设计了继承层次结构,并很好的实现了重用。而且,我们可以认定该接口是相对稳定的。我们再来看看实现部分:

public class PersonImpFlag implements Person, Salesman, Engineer,Manager{ // Implementing Salesman public static Salesman newSalesman (String name){ PersonImpFlag result; result = new PersonImpFlag (name); result.makeSalesman(); return result; }; public void makeSalesman () { _jobTitle = 1; }; public boolean isSalesman () { return _jobTitle == 1; }; public void numberOfSales (int value){ requireIsSalesman () ; _numberOfSales = value; }; public int numberOfSales () { requireIsSalesman (); return _numberOfSales; }; private void requireIsSalesman () { if (! isSalesman()) throw new PreconditionViolation ("Not a Salesman") ; }; private int _numberOfSales; private int _jobTitle; }



  这是其中一种被称为内部标示(Internal Flag)的实现方法。这里我们只是举出一个例子,实际上我们还有非常多的解决方法,但我们并不关心。因为只要接口足够稳定,内部实现发生再大的变化都是允许的。如果对实现的方式感兴趣,可以参考Matrin Fowler的角色建模的文章或是我在阅读这篇文章的一篇笔记。

  通过上面的例子,我们可以了解到,接口和实现分离的最大好处就是能够在客户端未知的情况下修改实现代码。这个特性对于分层技术是非常适用的。一种是用在层和层之间的调用。层和层之间是最忌讳耦合度过高或是改变过于频繁的。设计优秀的接口能够解决这个问题。另一种是用在那些不稳定的部分上。如果某些需求的变化性很大,那么定义接口也是一种解决之道。举个不恰当的例子,设计良好的接口就像是我们日常使用的万用插座一样,不论插头如何变化,都可以使用。

  最后强调一点,良好的接口定义一定是来自于需求的,它绝对不是程序员绞尽脑汁想出来的。

  数据映射层

  在各个层的设计中,可能比较令人困惑的就是数据映射层了。由于篇幅的关系,我们不可能在这个问题上讨论太多,只能是抛砖引玉。如果有机会,我们还可以来谈谈这方面的话题。

  面向对象技术已经成为软件开发的一种趋势,越来越多的人开始了解、学习和使用面向对象技术。而大多数的面向对象技术都只是解决了内存中的面向对象的问题。但是鲜有提到持久性的面向对象问题。

  面向对象设计的机制与关系模型有很大的不同,这造成了面向对象设计与关系数据库设计之间的不匹配。面向对象设计的基本理论包括耦合、聚合、封装、继承、多态,而关系数据模型的理论则完全不同,它的基本原理是数据库的三大范式。最明显的一个例子是,Order对象包括一组的OrderItem对象,因此我们需要在Order类中设计一个容器(各个编程语言都提供了一组的容器对象及相关操作以供使用)来存储OrderItem,也就是说Order类中的指针指向OrderItem。假设Order类和OrderItem分别对应于数据库的两张表(最简单的映射情况),那么,我们要实现二者之间的关系,是通过在OrderItem表(假设名称一样)增加指向Order表的外键。这是两种完全不同的设置。数据映射层的作用就是向用户端隐藏关系数据库的存在。

  自己开发一个对象/关系映射工具是非常诱人的。但是应该考虑到,开发这样一个工具并不是一件容易的事,需要付出很大的成本。尤其是手工处理数据一致性和事务处理的问题上。它比你想象的要难的多。因此,获取一个对象/关系映射工具的最好途径是购买,而不是开发。

  总结

  分层对现代的软件开发而言是非常重要的概念。也是我们必须学习的知识。分层的总体思路并没有什么特别的地方,但是要和自己的开发环境、应用环境结合起来,你还需要付出很多的努力才行。

  在完成了分层之后,软件架构其实已经清晰化了。下面的讨论将围绕着如何改进架构,如何使架构稳定方面的问题进行。

十、精化和合并



  对于一个已经初步建立好的模型(分析模型或是设计模型)来说,对其进行精化和合并是必要的步骤。

  Context

  建立架构愿景,为架构的设计定义了主要的设计策略和实现思路。应用分层的原则则对整个的软件进行了结构上的划分,并定义了结构的不同部分的职责。而现在,我们需要对初步完成的模型进行必要的改进。

  Problem

  我们如何对初始架构模型进行改进?

  Solution

  对模型进行改进的活动可以分为精化和合并两种。我们先从精化开始。

  首先,我们手头上的初始架构模型已经包括了总原则(参见架构愿景模式)和层结构(参见分层模式)两部分的内容。现在我们要做的工作是根据需求和架构原则来划分不同的粗粒度组件。粗粒度组件来源于分析活动中的业务实体。把具有很强相关性业务实体组合起来,形成一个集合。集合内部存在错综复杂的关系,同时集合向外部提供服务接口。这样的集合就称为粗粒度组件。粗粒度组件对外的接口和内部的实现是相区分的。粗粒度组件的形式有很多,Java平台上的Jar文件、Windows平台上的dll文件,甚至古老的.o或.a文件都可以是粗粒度组件的表现形式。设计优秀的粗粒度组件应该只是完成一项功能,这一点是它与子系统的主要区分。一个系统中可能包括会计子系统、库存管理子系统。但是提供会计粗粒度组件或是库存管理粗粒度组件是没有什么意义的。因为这样的粗粒度组件的范围过于广泛,难以发挥重用的价值。

  粗粒度组件是可以(可能也是必须)跨越层次的。粗粒度组件拥有持久化的行为,拥有业务逻辑,需要表示层的支持。这样看起来,它所属的轴向和层次的轴向是相互垂直的。

  粗粒度组件来源于需求。需求阶段产生的需求说明书或是用例模型将是粗粒度组件开发的基础。在拥有了需求工件之后,我们需要对需求进行功能性的划分,将需求分为几个功能组,这样我们基本上就可以得到相应的粗粒度组件了。如果系统比较庞大,可以对功能组再做细分。这取决于粗粒度组件的范围。过小的范围,将会造成粗粒度组件不容易使用,用户需要理解不同的粗粒度组件之间的复杂关系,最后的结果也将包含大量的组件和复杂的逻辑。过大的范围,则会造成粗粒度组件难以重用,导致粗粒度组件称为一个子系统。

  假设我们需要开发一个人力资源管理系统。经过整理,它的需求大致分为这样几个部分:

  ◆ 组织结构的设计和管理:包括员工职务管理和员工所属部门的管理。

  ◆员工资料的管理:包括员工的基本资料和简单的考评资料。

  ◆日常事务的管理:包括了对员工的考勤管理和工资发放管理。

  对于前两项的功能组,我们认为建立粗粒度组件是比较合适的。但是对于第三项功能组,我们认为范围过大,因为将之分为考勤管理和工资管理。现在我们得到了四个粗粒度组件。分别是组织结构组件、员工资料组件、员工考勤组件、员工工资组件。

  在得到了粗粒度组件之后,我们的工作需要分为两个部分:第一个部分是定义不同的粗粒度组件之间的关系。第二个部分是在粗粒度组件的基础上定义业务实体或是定义细粒度组件。
http://developer.ccidnet.com/pub/attachment/2003/6/196888.gif

 不同的粗粒度组件之间的关系其实就是前文提到的粗粒度组件的外部接口。如果可能,在粗粒度组件之间定义单向的关联(如上图所示)可以有效的减少组件之间的耦合。如果必须要定义双向的关联,请确保关联双方组件之间的一致性。在上图中,我们可以清晰的看出,组织结构处于最底层,员工资料依赖于组织结构,包括从组织结构中获得员工的所属部门,以及员工职务等信息。而对于考勤、工资组件来说,需要从员工资料中获取必要的信息,也包括了部门和职务两方面的信息。这里有两种关联定义的方法,一种是让考勤组件从组织结构组件中获得部门和职务信息,从员工资料中获得另外的信息,另一种是如上图一样,考勤组件只从员工资料组件中获得信息,而员工资料组件再使用委托,从组织结构中获得部门和职务的信息。第二种做法的好处是向考勤、工资组件屏蔽了组织结构组件的存在,并保持了信息获取的一致性。这里演示的只是组件之间的简单关系,现实中的关系不可能如此的简单,但是处理的基本思路是一样的,就是尽可能简化组件之间的关系,从而减少它们之间的耦合度。

  考虑另一种的需求情况,在原先的系统的基础上,我们又增加了会计子系统部分,其中的一个粗粒度组件是对部门、员工进行成本分析。在原先的模型基础上,我们增加了对分层的考虑。从下图中,我们可以看到,组织结构组件已经发挥了它的重用性,被成本分析组件使用了。从分层上考虑(参见分层模式),我们将组织结构组件划分到工具层,而将其它的组件划分到领域层,并在领域层中进行子系统级的划分。从某个角度上来说,这种做法类似于一个分析模型的建模过程。总之,这个过程中,最重要的就是定义好不同的组件的关系。尽管这中分析是初始的、模糊的。

http://developer.ccidnet.com/pub/attachment/2003/6/196901.gif

  在得到了粗粒度组件模型之后,我们需要对其进行进一步的分析,以得到细粒度的组件。细粒度的组件具有更好的重用性,并使得架构设计的精化工作更进一步。按Jacobson推荐的面向对象软件工程(OOSE)的做法,我们需要从软件的目标领域中识别出关键性的实体,或者说是领域中的名词。例如上例中的员工、部门、工资等。然后决定它们应该归属于哪些粗粒度组件。先识别细粒度组件还是先识别粗粒度组件并没有固定的顺序。

  最初得到的组件模型可能并不完善,需要对其进行修改。可能某个组件中的类太多了,过于复杂,我们就需要对其进一步精化、分为更细的组件,也许某个组件中的类太少了,需要和其它的组件进行合并。也许你会发现某两个组件之间存在重复的要素,可以从中抽取出共性的部分,形成新的组件。组件分析的过程并没有一种标准的做法,你只能够根据具体的案例来进行分析。到最后,你可能会为其中的几个类的归属而苦恼不已,不要在它们身上浪费太多的时间,尽善尽美的模型并不存在。

  最后的模型将会明确的包含几个经过精化之后的粗粒度组件。粗粒度组件之间的关系也会进行一次重新定义。如果这时候,粗粒度组件之间仍然存在着复杂的关系,也许意味着你的业务逻辑比较复杂,因此这个部分需要你投入比较多的精力来处理。当然,你可以通过一些技巧来减少不同组件之间的耦合程度。这里有几种可参考的办法:

  第一种方法是使用外观(Facade)模式(在分层模式中,我们就提到过外观模式)。如下图所示,新引入的BusinessFacade类充当了外观的角色,将调用者和复杂的业务类隔离了起来,调用者无须知道业务类之间的复杂的关系,就能够进行业务处理,从而大大降低了调用者和业务类之间的耦合度。这种方法在实践中经常被采用,适合用在内部关系较为复杂的组件中,也适合用在业务层向表示层发布接口的情况中。对于外观模式来说,我们可以在BusinessFacade类的业务方法中提供参数,来实现数据的传递。这对于一些数据较少的情景特别的适用。如果当数据种类较多时,也可以使用参数类或值类创锏绞荽偷哪康摹?
http://developer.ccidnet.com/pub/attachment/2003/6/196924.gif

  第二种方法是使用命令(Command)模式,该模式也来自于设计模式一书。在处理参数时,命令模式使用了一系列的set方法来逐一设置参数,最后调用execute()来执行业务逻辑。命令模式的好处是为调用者提供了统一的接口,调用者并不需要关心具体的业务逻辑,他需要做的就是设置数据,并调用execute()方法。如果遇到业务逻辑需要用到较多的参数,逐个的调用set方法过于麻烦了,也可以提供一个setValues()方法来处理多个参数。当然,该模式也有其弱点,如果业务方法太多,那么相应的Command类也会随之增多。这是我们不希望看到的。

http://developer.ccidnet.com/pub/attachment/2003/6/196913.gif

  除了上面介绍的两种方法以外,还可以使用诸如工厂(Factory)模式、业务代表(Business Delegate)模式等方法来减少不同组件之间的耦合度。应该认识到,不同的设计模式有其不同的上下文环境,在架构设计中使用设计模式(以及分析模式)有助于优化设计,但是请注意模式的上下文环境,误用或滥用模式反而会导致设计的失误。

  以上介绍的方法除了能够降低不同的组件之间的耦合度之外,还可以起到向调用者隐藏实现的功能。这一点对于重构活动(参见Refactoring模式)非常的关键,因为它可以有效的缓解在对组件进行重构时将变化扩散到其它的组件中。

  精化是对模型进行改进的第一步。完成的模型基本上代表了最终的软件。但如果我们对其进行认真的检查,我们会发现模型仍然存在问题。这时候的问题主要体现在设计模型过于肥大了。如果说精化使得模型变得复杂,那么合并就是使得模型变得简单。千万不要以为这两项工作是互斥的,通过这两项活动,可以使得模型得到极大的改进。

  还记得我们在简单设计模式中提到的简单原则吗?在进入下面的讨论之前,请确保你能够理解简单原则,这是敏捷的核心原则之一。

  在上文中我们提到了一些设计模式的使用,而在整个的敏捷架构设计的文章中,我们也大量地讨论了模式。而在很多时候,我们其实是在不恰当的使用模式来解决一些简单的问题。所以,在使用模式之前,我们应该回顾需求说明书(或是用例模型)上的相关部分,确定是否需要使用模式。

  在一次的设计软件的持久性机制的时候,我选用了DTO(Data Transfer Object)模式作为持久层的实现机制。原因只是我在前一天晚上看了这个模式,并觉得它很酷。看起来我是犯了模式综合症了(当学习了一个模式之后,就想方设法的使用它)。在花费了很多的时间学习并实现该模式之后,我发现该模式并没能够发挥它应有的作用。原因是,模式的上下文环境并不适合用在目前的软件中,原本只需要用JDBC就可以实现的功能,在使用了模式之后,反而变得复杂了。糟糕的是,我不得不向开发人员解释这个模式,吹捧这个设计模式的精妙之处。但是结果令人不安。开发人员显然不能够理解这个模式在这里发挥了什么样的作用。最后,我去掉了这个模式,设计得到了简化。

  同样的,在开发过程还存在各种各样的过度设计的例子,尤其是数据库访问、线程安全、一致性等方面的设计。这些问题往往需要花费大量的时间来处理,但是他们的价值却并不高,尤其是小型的系统。当然,在设计一些大型的系统时,这些问题是必须要考虑的。

  而当使用设计模式在对不同的组件进行整合的时候,我们也需要对组件的行为进行合并。将不同组件之间的相同的行为合并到一个组件中,尤其是那些关系非常复杂的组件。这样可以把复杂的关系隐藏到组件内部,而简化其对外提供的接口。

  很难评判设计是否已经完成了。这里有两个不同的极端。花费了过多的时间在初始设计上,以及过度的迭代。在初始模型上花费太多的时间并不一定能够得到尽善尽美的模型,相反的,可能还会因为设计师钻牛角尖的行为导致设计模型的失误。而在过于频繁的迭代对于改进模型同样没有好处,因为实际上,你不是在改进模型,而是在改变模型。请区分这两种完全不同的行为,虽然它们似乎很相似。在Refactoring模式中,我们还会进一步对迭代和模型改进进行讨论。

  一种判断方法是请编码人员来评判设计是否完成。设计模型最后是要交给编码人员,指导编码人员的开发工作的。因此,如果编码人员无法理解模型,这个模型设计的再好看,也没有太大的作用。另一方面,如果编码人员认为模型已经足够指导开发工作了,那么还有什么必要再画蛇添足下去了呢?不同水平、不同经验的编码人员对模型的要求也不一样。在我们的工作中,对资深开发人员和开发新手的发布的模型是不一样的。对于资深的开发人员而言,可能只需要对他说,在组件A和组件B之间使用外观模式,他就能够理解这句话的意思,并立即着手开发,可是对于没有经验的开发人员,就需要从模式理论开始进行讲述。对于他们来说,设计模型必须足够充分,足够细致。设置需要把类、方法、参数、功能描述全部设计出来才可以。

  复审是避免设计模型出现错误的重要手段。强烈建议在架构设计过程中引入复审的活动。复审对于避免设计错误有着重大的帮助。复审应该着重于粗粒度组件的分类和粗粒度组件之间的关系。正如后续的Refactoring模式和稳定性模式所描绘的那样,保持粗粒度组件的稳定性有助于重构行为,有助于架构模型的改进。

posted on 2007-04-29 09:34  IT诗人  阅读(323)  评论(0编辑  收藏  举报

我要啦免费统计