应用系统架构设计
原文地址:http://simonw.cnblogs.com/archive/2005/10/27/263145.html
我们在做着表面上看似是对于各种不同应用的开发,其实背后所对应的架构设计都是相对稳定的。在一个好的架构下编程,不仅对于开发人员是一件赏心悦目的事情,更重要的是软件能够表现出一个健康的姿态;而架构设计的不合理,不仅让开发人员受苦受难,软件本身的生命周期更是受到严重威胁。这里我将针对在微软dotNet平台上做应用开发的系统架构设计做一个粗浅的讨论。
总体设计图
表示层
表示层由UI(User Interface)和UI控制逻辑组成。
UI(User Interface)
UI是客户端的用户界面,负责从用户方接收命令,请求,数据,传递给业务层处理,然后将结果呈现出来。根据客户端的不同我们大体将应用程序分为BS(Browser-Server)浏览器结构,CS(Client-Server)桌面客户端结构。
BS的优点是无需操心客户端,只需要部署维护好服务器即可。CS的优点在于强大的界面交互表达能力。RIA(Rich Internet Application)是为了融合这两种结构优点的一种技术,它依赖在客户端一次性安装一个通用解释器之后即获得强大的界面交互表达能力和无需部署具体客户端的方便性。具体的实现技术很多,例如微软的SmartClient, Avalon; Macromedia的Flex;以JS为基础的Bindows;Ajax等等很多。
UI控制逻辑
UI控制逻辑负责处理UI和业务层之间的数据交互,UI之间状态流程的控制,同时负责简单的数据验证和格式化等功能。具体的说在dotNet事件驱动的编程模型下,UI控制逻辑被自然的实现在了事件函数中,例如PageLoad事件函数,ButtonClick事件函数。在这些事件函数中,主要任务就是做UI控件与业务实体的数据交换与业务调用,但面对大量的数据交换工作量与维护量就成了最大的问题。而在复杂应用的系统中,状态与流程的管理是必须要考虑的因素,它包含了界面与业务两方面。如果不加以封装的直接写在事件函数中将导致业务依赖表示层。下面分别讨论这两个问题。
1.UI与业务实体之间的数据交互
此阶段负责数据交换的业务实体我把它称为DTO(Data Transfer Object),但需要说明的是这里的DTO并不是只包含数据的业务对象,它仍然包含必要的方法是完整的业务实体。处理输入时我们从UI控件的获得数据填入DTO再向下传播,处理输出时用户发出请求业务层会将数据以DTO的形式返出再赋给UI控件展现。因此需要一种方式来自动解决这样的来回赋值问题。Java下Structs的Formbean对此问题提供了一定支持,而遗憾的是dotNet下的不少控件虽然支持数据绑定但仍然没有一个现成完整的解决办法。一种比较简单的方式是自己设计一个Adapter按照某种映射关系来自动处理这样的绑定,这样的映射关系最好是UI控件与DTO属性的事先命名约定,以此种方式的约定作为映射关系无需增加任何配置文件和配置工作即可实现。例如你的一个输入人名的Textbox命名为txtUserName,而我的业务实体属性命名为UserName,这样就可以通过字符串的查找来找到对应。
2.状态与流程的管理
复杂业务方面的状态与流程可以通过一些工作流引擎来解决,微软最近独立发布了自己的工作流引擎有兴趣的朋友可以去看看。一般更多的情况是需要解决界面上状态与流程的管理。耦合再表示层中是不可取的办法。MVC(Model-View-Controller)模式提供了实现这一目标的方法。Controller是整个方案的核心,它是一个流程管理器,来自UI所有的命令与数据经过Controller分发给业务层或其他UI,这样我们可以把流程,权限等逻辑单独封装,例如配置文件中,达到最大化的业务重用。dotNet下MVC的方案并不像Java下有那么多选择,目前有以下几种选择:
微软的UIPAB,它可以处理bs,cs下的流程跳转,可以使得相同的业务系统有webform和winform不同的展现方式。
开源的Mavrick.Net,它只适用于Asp.Net应用程序,它对流程,国际化,页面包装,xslt页面转换提供了很好的支持。
开源的Lattis,功能比较单一,同样只适用于Asp.Net应用程序。
业务层
业务层封装了实际业务逻辑,包含数据验证,事物处理,权限处理等业务相关操作,是整个应用系统的核心。因此设计一个能够真实反映实际需要的业务层是非常必要的,我们将实际业务具体分为业务数据与业务操作两部分。
业务数据
业务数据又是业务逻辑的核心,最终业务数据将以一种固定的格式表现于内存中,在系统的各个层次间传输,充当DTO角色。表达业务数据的方式一般分为两种Table Model和Domain Model。
Table Model是将数据库中的表直接映射成为业务数据对象,这样的优点是适合于机器操作,ADO.NET直接提供了这种操作的便利,但对于复杂业务关系的表达就很不直观。只适合于业务需求与数据表对应关系很直接的需要快速开发的情况。通常我们选用Dataset或者强类型Dataset(Strong Typed Dataset),强类型Dataset支持编译时的类型检查,效率上要略高于普通Dataset。Dataset有很多方便的特性:无需自己编写维护类,支持序列化,数据副本保存,支持数据集合,对控件绑定支持效果好,微软提供了相应的生成工具以及持久方案。但缺点也是明显,复杂数据表现不直观,做为DTO在各个层次间传输,尤其是分布式环境,庞大的体积,相对缓慢的实例化对于性能造成很大压力。
Domain Model则是根据实际业务按照现实方式用OO思想建模,这样很适合业务复杂的系统。通常采用自定义数据实体(Custom Data Entity)方式表达。自定义数据实体,有着良好的性能,编译时的类型检查,数据表现方式非常直观符合实际业务的操作方式等优点,但需要自己定义维护类,在分布式环境下需要自己编写序列化方法。
综合各种因素考虑,虽然业务简单对应直接的系统我们以Table Model建模开发效率很高但难免保证系统日后不会变的复杂,因此出于复用性,扩展性,性能等方面选用Domain Model建模为佳。
业务操作
业务操作负责对业务数据进行各种业务相关的处理,例如验证,流向,整合,事物,权限等,但它不负责有关对数据源的操作。它与业务数据的关系设计有2种方式。
分离业务数据与业务操作,将业务数据单独封装到只有数据get,set的数据类中,这个数据类只充当DTO。将业务操作封装到独立的service类中与业务数据一起充当业务层。这样当系统不复杂的时候显的简单直观,而随着系统日益复杂,service类会变的杂乱,而将本身耦合紧密的数据与操作分离对于复用也是不利的因素。具体可参考Martin Fowler 的贫血的Domain Model一文。
整合业务数据与业务操作,将业务数据与相关的业务操作封装在一起称为业务实体,业务实体作为统一的业务层为表示层提供服务,同时也负责作为DTO在各个层次间传输,我倾向于这样完整的Domain Model设计方式,每个业务实体都可以做为一个单独组件形式存在,对于组件化复用有着莫大的好处。
业务数据访问层
业务数据访问层是一个针对具体应用系统的专属层,它为业务层提供与数据源交互的最小操作方式,仅仅是业务层需要的数据访问接口,业务层完全依赖业务数据访问层所提供的服务。这些服务负责从业务层接收数据或返回业务实体,它屏蔽了实际业务数据与机器存储方式的差别。当然,数据层选用抽象的解决方案同样可以达到这个效果,但业务数据访问层最大的特点就是针对具体业务做抽象,而抽象的数据层访问方案是针对通用做抽象。往往业务中针对具体的设计生命力会变的更强,这样我们可以最大限度的保持了上层代码的复用性,当需要更换存储策略如果数据层访问差别太大,通过更换数据层无法解决问题的时候我们最多只需要更换业务数据访问层,而无需改变业务层。
业务数据访问层由DAO(Data Access Object)层和系统服务层两部分组成。DAO层为每个业务实体提供最基本的数据访问服务,系统服务层为系统全局提供与业务关系不大的通用数据访问服务,这两层处于系统中的同一个层次位置。
业务层与业务数据访问层关系图
数据层
数据层的宗旨就是为数据源提供一个可供外界访问的接口,我们应该选用一种能够提供数据源无关的抽象数据访问接口并通过在其下挂接各种不同的DataProviador来访问数据源的数据层组件,这样做便于移植到不同的数据源上。目前有以下3种数据层方案:
1. 封装ADO.Net
这些数据访问组件都是基于ADO.Net的浅封装,它的优点在于封装层次低所以速度最快,我们可以手动组织sql语句用来适应复杂的操作以及个性的优化等。缺点是无法直接处理自定义数据实体方式的业务实体对象,需要将业务实体中的数据属性以参数形式传入传出。这样的方式虽然最为保险,但随着系统规模增大,开发效率,质量,,后期的维护,二次开发都变成尤为突出的问题,对开发人员的要求会变的越来越高。另外对于事物操作封装不是很好,无法提供声明性事物,经常会在业务层出现访问数据层的需要。这样的组件目前应用的很广泛,例如微软在EnterpriseLibrary中提供的DAAB(Data Access Application Block),还有以前的DAAB3.1。EnterpriseLibrary是个成熟的产品,包括了数据访问,异常,日志,缓存,加密,配置,安全等组件做为通用服务非常适合。
2. OR-Mapping组件
ORM是最好的数据持久解决方案,它的优点在于能够以面向对象的方式操纵数据,因此可以直接处理自定义数据实体的业务对象,我们根本不用操心sql语句以及底层存储方式,这样极大的简化的代码提高了开发效率,对于日后维护扩展都带来极大的便利。缺点在于屏蔽了底层使得我们无法针对具体数据源做优化,而且对于复杂关联的sql操作有些力不从心,同时性能也差一些但辅助以缓存情况会好很多,而在dotNet下最大的问题就是没有一个成熟便宜的ORM产品供我们使用,全部都是beta版本和商业版本。这些版本或多或少都存在一些问题,以至于真正应用中需要经过仔细考察。例如NHibernate,Gentle.Net,XPO,Grove.Net等等非常多。
3. DataMapper(SqlMapper)
SqlMapper为以上两种方式提供了一个折中的选择,它可以以面向对象的方式直接处理自定义数据实体的业务对象,同时可以根据与数据源与业务实体的映射关系执行手写的sql语句,这样完全使得我们可以针对具体数据源做优化,对于复杂操作同样可以胜任。目前只有iBatis.Net一个产品,它是一个java移至的开源项目,已经比较成熟,可以在无需编译的情况下随意替换DAO。
各层间的依赖关系
依赖关系图:
在对各层的讨论之后,我们来总结一下各层间的依赖关系。说到依赖就离不开复用这个词,复用对软件开发流程的几乎每个阶段都有着重要的意义。在设计阶段它代表着更清晰的设计,在开发阶段它代表着更高的工作效率和代码质量,在测试阶段它代表着更轻易的捕获bug,在维护和再开发阶段它代表着更小的工作量。更好的复用需要设计更好的依赖关系。
从图中可以看出表示层与业务数据访问层都依赖于业务层,而业务层是相对独立的,这样设计的优点就是最大限度的减少了变动对整个系统所带来的影响。最坏的情况就是业务的改变,业务改变其他依赖业务层的地方必须改变(在这里我们忽略了一些针对多种业务而设计的其他层组件,这些组件是可以适应有限的业务变更而本身不用变更的。),这个我们没有办法控制。但像表示层与业务数据访问层等其他非业务方面的改动不会影响到其他地方。
有人应该注意到了图中的配置文件。在没有它的时候,业务层是依赖于业务数据访问层的,细心的读者应该能从业务层与业务数据访问层关系图中发现这个问题。这样双向依赖的关系是以后造成无法复用的根本所在。因此需要抽象出业务数据访问接口,让业务层去依赖这个接口,而不是业务数据访问层。但光声明接口是不够的,因为在实例化的时候仍然需要具体的下层类,所以依旧无法摆脱依赖关系。于是把依赖转移出来,这又引出了依赖倒置(Dependency Inversion Principle)的概念,具体可以参见Martin Fowler的相关论述。IoC(Inversion of Control)容器为我们提供了完美的方案,通过它将不同的模块注入到系统中我们可以在不知道这个组件存在的情况下调用它。这样的方式同样适合于权限管理,邮件发送等等其他组件。Spring.Net和Castle是dotNet下的两个优秀的IoC容器Spring.Net是Java下Spring的移植版本,Castle相对更要成熟。但是当你使用的组件并不是很多而不愿使用配置这些复杂而强大的产品时,你就要手工完成这些工作,你需要把业务层使用那些数据访问组件写在配置文件中,然后通过工厂(Factory)解析配置文件应用反射(Reflection)技术实例化出你的组件。
最后再说点关于AOP(Aspect-OrientedProgramming)的话题,在一些非常通用的组件或者系统功能间我们可以使用AOP技术来打散系统其他部分对他们的依赖。像权限管理,系统日志,异常处理等。拿权限管理来举例,通常我们是在需要做权限检测的函数内部的开头来加一行权限检测代码,通过则执行后续代码否则跳出。这样写破坏了业务的纯洁性,业务的存在于权限并没有什么关系,而且使业务代码依赖权限组件,当我需要去除权限而另做新系统的时候这是见很麻烦的事情。AOP的好处在于可以以声明方式来处理这些问题,例如你只需在需要验证的函数前加上一行属性的描述或者在配置文件中写上那些函数需要验证,执行时AOP组件会按照你预先定制的先后顺序执行你的代码,这样我们可以轻松的剥离这些组件而丝毫不会对现有系统造成任何影响。
关于Service层的讨论
Service层是一个构建于表示层于业务层之间的层,它是对业务层的一个浅封装而不应该封装过多的业务逻辑,否则会造成不必要的麻烦。然而它并不是任何时候都有必要存在。一种情况下当你的UI所要展现的东西和你的业务实体并不是那么完全吻合的时候,例如你的界面需要显示若干个业务实体的一部分,但这并不是业务本身,只是一种展现方式。这时候你需要Service层来做一个展现逻辑的转换。或者当你在做分布式系统的时候可以利用Service层来实现一个粗粒度的服务接口。也可以以SOA(Service-oriented Architecture)的方式来理解Service层,我们最终使用的是系统所提供的服务而不是业务对象,所以需要将将业务对象以清晰的方式组织起来形成清晰的服务暴露在外。更多的情况在于你结合实际该如何使用。