作者:Anders小明
2009年5月5日
五、架构的技术层面
(一)基础手段
提高开发效率和品质的基本手段是分解——即充分的分离系统中不同的关注点,好处不用说了,可以并发的工作,每个人面对的问题都简单而容易操作。而与分解对应的集成,只有提供了好的集成能力,分解才成为现实,而只有分解了,才能清晰的提供业务更多适应性。
分解和集成的手段分为编程语言和技术框架两个层面。所谓语言就是强框架,而框架就是弱语言。
A. 开发语言
现代面向对象的语言提供如下能力:抽象和派生能力,以及接口隔离能力。实际提供两种分解和集成能力:
1. 把逻辑分解在两个层次中,而通过继承的方式把两个部分集成在一起。
2. 把逻辑的外观和实现分解在两个地方,而通过接口实现的方式把两部分集成在一起。
另一种语言AspectJ或者C#语言2.0之后提供的特性:把流程逻辑,分解在不同的地方,而通过签名匹配,利用代码生成的方式来把几部分集成在一起。
B. 应用框架
然而语言提供的集成能力,毕竟底层,而且有限,扩展起来也格外小心。因而技术框架提供另外的集成能力就格外重要:
1. 对象关联关系的分解和集成,如Spring提供容器管理能力
依赖注入对于依赖关系是适合的,对于服务间,技术层次间都是适合的(因为无状态);但对于聚合(整体和部分)的关系——主要是领域模型(有状态的)——则不合适;
2. 模块间关联关系的分解和集成,如OSGi,ESB等
3. 流程逻辑的分解和集成,如Spring Web Flow以及jBPM。
4. 不同系统的类型分解和集成,如Spring利用动态代理提供的Exporter模式。
5. 模式的封装集成,设计应当是面向服务的设计,但是服务的暴露方式以及模式可以有很多种,比如API,Web Service,RMI,以及Command模式,Event模式等,框架应该利用动态代理等技术对于这些服务暴露方式,模式进行封装。
C. 分析设计
设计中涉及到的组合方式,包括类(接口)组合,继承组合以及产生组合三种。三种组合各有优缺点,设计时适应不同场合。这就涉及到现有面向对象的设计粒度:类(第一公民)和方法(二等公民)。
类(接口)组合实际上复用的是类一级粒度的设计,而继承组合本质上是一种有方向的组合,复用的方法一级粒度的设计,提供与或非的逻辑操作。而产生组合,例如AspectJ,也是在方法一级粒度的设计复用。
因为继承组合复用在方法一级的粒度上,因而其更适合存在嵌入式,最低粒度的差异性的设计中,借助于虚拟机的支持,无需额外工作。而类(接口)在类一级上,更适合在更高一级的逻辑复用上;其实不一定需要接口,普通的类也可以,但是在这一级粒度的差异性替换,采用接口优于类,因此称为类(接口)组合;接口是类(接口)组合的编码需要;对于接口一级,需要通过框架的集成和适配来提供差异性的设计。产生组合其实也是在方法一级,不过更关注于广泛的横切面,同时由于现有的语言对它的支持不同,Java需要额外的编译器,而.Net则是在内置编译器上支持。
更高一级的组合是组件组合。对于组件边界的设计,遵从两点:严格把关设计和代码优先。接口优先的设计通常导致成本太高,实践中会导致开发人员在项目的进度压力下把代码写在不合适的地方。
D. 开发方式
常见的开发方式可以归结为3类:开发式编程(Programmatic programming),声明式编程(Declarative programming)和产生式编程(Generative programming)。
|
开发式编程
|
声明式编程
|
产生式编程
|
开发手段
|
编码。
如:Java, C#
|
解析。
如:ANT(spring等的xml不一样,它们是静态描述型的,不那么verb)
|
生成。
如:AOP(AspectJ),DSL(Drools)
|
开发性质
|
聚合
|
声明
|
组合
|
代表事物
|
接口
|
N/A
|
DSL
自然语言的表达能力很强大,虽然说有时具有二义性,但是在特定领域下是确定的,既然是讲DSL,那都是特定领域相关的,一定是明确的。
|
基础设施
|
|
解析器;
编辑器,如jbpm;
元模型;
|
生成器;
正统的需要编辑器;
元模型
|
开发方式
|
|
自上向下,声明式编程是解析概念,用统一的概念来理解,把不同差异性交由具体程序解析;
编辑器生成的是xml文件,将由框架程序解析;
声明式是粗粒度的(不能直接比较大小,定义的是无差异性的概念);
|
自底向上,产生式编程用的思路是组合概念(用小粒度的概念组合生成大粒度的概念);
产生式生成程序代码,不做解析运行;
产生式是相对细粒度的;
|
E. 小结
通常语言作为架构的基础,语言的设计带来的好处远远高于框架和模式,但其引入和更换也是有巨大风险的;而通过提供强大的框架能力,框架尽可能多的完成技术问题,并通过元数据,模式以及约定降低业务和框架的耦合。避免因为框架升级带来不必要的成本。
Meta Programming的最高层次是语言级别直接解决,比如,Smalltalk, Ruby, Python, 还有其他Reflection支持的非常好的语言。甚至STL等template技术,也可以算作语言级别。 Code Generation 是最低级别的Meta Programming解决方案,技术含量也最低。这个级别必须超越,才能够真正达到质变,完全跳出概念炒作的层次。
从技术手段上,提高开发效率的另外两个手段是代码生成和类库引用。但代码生成和类库引用,都只解决了逻辑的分解能力,没有提供集成能力,所以一般情况下需要提供框架集成,尤其代码生成需要在系统的最外层,避免集成带来的问题。
代码生成也没有那么坏,关键在于生成什么,如果是生成结构性的代码,由于往往不是最终的产物,就存在同步维护问题;同时这种代码是大都可以用template完成的。
但如果生成的是功能性代码,这类代码是最终执行代码,那么通常就把用于设计的代码看作是最终产物,最明显的例子是DSL。
(二)核心问题
1. 领域化
领域化,即领域建模。通常而言,领域模型设计中,模块分解,抽象分层和职责分层都是重要手段。问题域为:流程,领域模型和领域服务(包括规则)。
a. 对象的抽象分解和集成
b. 对象的依赖分解和集成(模块内和模块外)
c. 流程的分解和集成(页面流,工作流以及计算流程)
d. 进程边界:用户请求重定向,以及业务数据持久化等。
对于中等项目来说,系统中应该有50-100个领域对象代表了业务抽象;
2. 组件化
面向对象语言本身没有提供的组件级别的依赖关系集成能力。语言不提供,因为领域组件的粒度太大,超越了语言的范畴。但我们可以通过框架提供,在Java体系中,目前已经有两个较好的解决方案:OSGi(JSR291)和SCA。可以很好的解决组件服务依赖关系管理,包括热替换。
同时另一个问题——逻辑分层的问题:如保险产品面临的核心层,国家层以及公司层三个逻辑层次分解和集成能力。这点的解决方案可以通过OSGi + Spring来解决,包括了静态差异性替换和动态差异性替换。
还有组件边界保护问题,我们希望限制别的组件访问本组件内部实现,有两种手段可以完成,1是提交/部署时,通过在代码提交时的代码检查工具,或者发布时编译工具完成;2是通过OSGi的边界限制能力。
3. 产品化
A. 定制化支持
领域定制化涉及到逻辑替换问题。逻辑的替换根据开发方式不同,有两种类型:基于接口和基于继承;
A. 基于接口(包括了静态替换和动态替换)
1. 静态替换是Override,在OSGi中只要停止原有服务,启用新服务即可,而在Spring中更改相应配置文件即可;
2. 动态替换,其实是指运行时Condition Service Locator,在OSGi中可以利用Extension Point(Plug-in)解决,而Spring中只要提供一个类似Service Locator就可以。
B. 基于继承(或者静态类)
1.开发时,直接修改源代码编译;
2.编译时,采用AspectJ,在编译时提供替换;
3.加载时,开发一个新逻辑的同名类,但其加载路径优先于原有类;
B. 升级支持
主要是增量升级支持,以及有限的降级支持。同时要考虑到对于定制化产品的升级支持。
4. 平台化
A.基础设施
基础设施包括:类库和框架。基础设施可以自己开发,或者应用第三方(开源商业)实现。
A1. 基础设施的选型
应考虑几点:1. 商业角度的可维护性和可升级性;2. 组织的学习和管理能力;3. 基础设施自身功能以及所支持的开发效率。以下是详细要求:
客户角度
|
成熟度要求
|
基础设施是业界成熟方案;
|
性能要求
|
基础设施满足系统运行的性能要求;
|
稳定性要求
|
基础设施版本稳定,经过大量测试;
|
环境性要求
|
基础设施不会带来额外的软硬件兼容要求;
|
管理角度
|
开发成本要求
|
基础设施的开发维护成本低,最好是业界成熟开源成果;
|
开发效率要求
|
基于该基础设施的应用开发效率高;
|
维护成本要求
|
分析设计与开发之间的衔接性好;
|
测试成本要求
|
基于该基础设施的应用测试成本低,效率高;
|
培训招聘成本要求
|
网络上的参考资料丰富性;基础设施的流行度;
内部员工学习培训成本低; 招聘外部员工成本低;
|
A2. 基础设施的集成
基础设施独立后,出现平台化的发展趋势,这个趋势有两个方向:通用化和专业化。通用化意味着基础设施和应用的距离加大,易用性减低;而专业化意味着适应性的减少。这是一个矛盾体。在基础设施选型后,再进行一定集成工作,可以结合当前情况,平衡易用性和适应性;同时合适的集成也有助于隔离技术和业务两个方面。
从维护升级角度看集成的合适性:对于没有标准的,不要做不必要的封装,封装等于是建立一个标准,而这是不现实的;应当尽可能采用框架方式,屏蔽基础设施对于应用程序的侵入性。如果是标准,就更没有必要封装,画蛇添足。
B.业务支持
B1. 基本原则和手段
基本原则是:应用程序POJO化。减少技术对于业务侵入性。主要手段是:容器上下文;依赖注入;AOP技术;元数据支持;事件机制;开发工具和代码生成;
依赖注入+AOP+元数据构成了简单对象(POJO)的支撑技术。基于此三位一体的技术可以有效的隔离业务问题和技术问题,更为甚者它可以支撑简单对象体系,每个对象做且只做一件事。
B2. 开发模式与最佳实践
基础平台应该提供业务相关的模式封装。
B3. 关于元数据
元数据有多种:语言级别为Annotation(微软.NET为Customer Attribute);框架级别可以是XML文件或者其它配置文件。
元数据可以通过以下几个视角观察
1. 应用层次:元数据代表了业务含义和技术含义;
2. 技术分析:文档类型(开发管理型);编译类型(类加载型);运行期行为。
3. 物理分析,包括Annotation和接口,XML文件,甚至是EL和类。
元数据系统的建立其实是代表了认知过程。
以运行期的元数据为例,代表了系统通过反射获取相关元数据来自适应系统,其实际意义在于将软件设计开发人员对于系统的认知通过技术手段固化下来。
元数据系统的开发目的有两个:
1. 业务应用上,提供业务动态能力;
2. 技术应用上,简化开发减低成本;
这里面有一个误区是:为了技术应用而过分地开发元数据系统,而随着业务的演化导致为技术应用的元数据迅速被抛弃,导致投入的浪费。实践中要避免。
(三)应用问题
1.事务管理
A. 成熟的事务技术:如数据库;
B. 合理的并发设计控制;
C. 完整的业务日志;这也是解决业务回退的主要手段;
D. 辅助的数据校验能力;
并发设计控制和完整的业务日志,是架构设计中保障数据一致性主要着力点。并发设计控制,需要结合业务,通过悲观锁定来保障。
而业务日志的获取则面临着诸多困难,主要是业务事务和物理事务的不一致性(即一个业务事务可能横跨多个物理事务,也可能一个物理事务包括多个业务事务);业务日志控制层面有两个:应用系统或基础设计;通过应用系统编码控制,则不可避免的提高了应用系统开发和测试的成本;通过基础设施控制,有助于减低成本,但提高基础设施的设计成本;
2.并发处理
分析业务所涉及的并发场景,制定相应的原则和方法,并合理选用现有的并发处理框架,进行一定程度的剪裁,通过框架支持和简化这些原则和方法的实践。
3.系统分解
基于应用的层面的分解,有多个纬度,包括:业务抽象度,业务任务,业务产品线,以及业务领域等等。
4.集成能力
软件开发的适应性在于分解粒度的大小,而分解粒度大小取决于集成能力。
1)依赖管理
在技术的角度看,软件系统是一个存在大量依赖关系的对象系统。其中包括了两种依赖:
1.业务代码的对象依赖;比如调用一个工厂类创建一个对象。
2.业务代码的环境依赖;它可能依赖于一个Web环境(读写Request和Response流),数据库系统(读写数据记录),文件系统和网络系统等。
不幸的事是这种代码量占据了大量的开发工作。重复的开发工作(对象或者数据的依赖关系维护工作)减低了开发效率和系统适应变化的能力。
而这样复杂依赖关系也给软件的测试带来了相当大的困难需要搭建足够的依赖环境(如一台Web服务器和数据库服务器),甚至是硬调试。
于是就有了采用第三方代码来完成依赖关系维护工作的思路,所谓的依赖注入。业务对象出现Spring等著名框架
动态代理技术则解决了提供支撑环境封装的问题。比如提供网络访问能力(如RMI,URL和Web Services),文件访问能力(如xml、property文件读写)。
由于企业开发中数据库技术的应用不可避免,因而ORM框架的出现还有特别的意义,在它的支撑下,核心业务西可以严格区别领域逻辑和业务逻辑,而这在以前是做不到的。
AOP开发的出现解决了面向对象下的横向组织关系。从一定程度上看,AOP可以看作是另一种依赖关系,可以另外依赖注入来实现。当然也可以采用编程实现。
2)数据对象
说起集成,就不得不提到一种类型的对象存在——VO对象。
VO对象是为了集成而存在的;其意义是:1. 保护系统的信息边界,提供一种结构可以使其它系统或者组件通过编码方式获取系统内信息的方式;2. 保护系统的事务边界,领域对象技术上携带着持久化信息,通过VO可以屏蔽得以屏蔽。常见的VO对象存在于Web层和Domain层。
因此,VO对象的存在只是为了集成而存在,其是否存在的取决于两个方面1. 集成的设计结构;2. 框架的两个能力——对象路径访问能力以及事务边界管理。
Domain层VO对象,通常是用于不同领域组件间的交互,但随着架构的改进,集成代码独立存在而不再嵌入到组件内部,组件的边界问题保护不复存在;更进一步的是,框架提供自动化的接口适配映射能力的增强。因而VO对象失去存在的意义。
Web层VO对象,以SWF为例,早在SWF 1.x时代,框架就提供了丰富的对象路径访问能力,但其Web交互是典型的MVC2方式,事务边界在view的render前关闭,因而导致需要特定的VO对象来避免持久化信息问题;而SWF 2.x时代,view的render是在事务边界内,VO不再需要。
系统设计是一种结构化过程,逻辑和数据被分解和集成到系统的各个部分;在运行期,真正重要的是结构化路径访问能力,换句话说重要的是结构化后的路径,而实际用何种数据结构其实不重要。
可以使用数据树Data Tree形式,提供扁平化的数据访问能力,是一种较好的开发方式,极大的提升了开发效率。Spring Web Flow以及其它框架广泛的运用EL提供统一的表达式访问数据,也大大降低了开发成本。
3)事件机制
事件机制应用非常广泛,是很重要的集成手段。事件机制的优势在于其提供了松散耦合而带来的扩展能力。基于传统事件模式,可以扩展提供同步/异步,事务隔离等额外控制能力。
4)组件设计
一个组件包括了API和SPI,其中API是用于客户方编程,SPI用于服务方编程(属于框架回调)。无论是API和SPI都是该组件所有,体现了一个组件自身的完备性。其与其它组件依赖通过集成模块完成,依赖解藕。
组件的设计还分层次,上层组件的逻辑依赖下层组件,上层组件直接访问下层组件的服务和模型,保持单向依赖有助于降低开发和维护成本。而平级组件,由于组件的替换可能性大,因而保障组件边界完整性尤为重要。
接口的实现是关键。面临的问题是,在开发初期需求不确定和经验不足的情况下,接口的设计不尽合理,导致需求变动后,所进行的修改将影响三个方面,接口、接口实现对象和测试用例。工作量将可能很大。特别是在并行开发过程中,一个通讯接口的变化将可能引起很大连锁反应,导致其他成员不得不停下手上的工作。
因而在实际开发中需要做个权衡。不同模块的通讯接口应该由团队成员共同负责,一旦接口变化,接口实现成员应该提供相应的假实现。而模块内部可以由开发人员自行设计,可以在初期不提供接口和简单的测试用例,在项目具有一定稳定性后,利用重构实现接口和完整的测试用例。
有效定位系统错误。尤其在组件化和分层化,以及其它开发手段混合运用情况下。例如,A,B,C。由于C引起的错误导致A错误是很难查的。代价很高。
5)胶水层
胶水层代码属于集成范畴。它是系统开发中不可避免的。胶水层代码的存在增加了设计、开发和测试的成本。因尽量减少胶水层代码的人工开发。
胶水层代码有两种类型:一是适配器,二是开发模式。
适配器对于集成来说并不陌生。适配器从用途上分可以分为两种:业务适配和技术适配。业务适配是指处理应用程序接口的适配调用,消除应用程序的耦合度;技术适配是指处理应用程序和技术框架上,消除技术框架的耦合度。
开发模式是指基础平台对于应用开发的支持减少无效代码,例如采用ORM系统(如Hibernate)以及有状态的Web层框架(如Seam和SWF)可以有效减少应用系统处理数据(对象)状态;以及各种元数据的识别和增强。
6)集成阶段
根据阶段分为:设计时, 编译(加载)时和运行时。设计时是由人工编码,通常就是一些特定业务代码,完成集成工作;编译时集成工作通常指配置文件,由程序员提供,但不需要编码工作;而运行时指通过指定元数据,由框架运行时解析;
5.部署方式
部署有两种:本地部署和分布式部署。
分布式部署会带来额外的问题。如果支持分布式部署,有两种方案:
1. 前后端分离方案
传统EJB方案就是此种。面临的问题和风险:
a) 部署成本,即分布部署带来开发成本;
包括:分布式调用;分布式事务管理;开发模式(即上下文的传递)。
b) 运行成本,即分布式数据传输性能问题;
2. 采用Portal或类似技术
(四)设计问题
设计文档依然有用,采用Color UML有助于阅读。
面向对象提供各种关系的表达能力:关联,依赖,集成,组合等;类似于数据库表关系,但是更强烈。在设计时需要注意表现。
新的开发方式,应该可以通过代码记录分析设计的成果,形成系统中一个稳定的可发展的抽象层次代码,而开发实现则继承或者适配该抽象层次,最终保证系统可运行性。
这样知识层面的分析设计可以有效贯彻发展,而操作层面的开发实现可以关注于实现过程的工具和手段。这样就可以确保设计是做正确的事,而开发过程提供各种框架工具则把事做更有效率。
六、架构的展示
1.两个要素
架构要展示的两个基本要素是:业务和技术组件。而业务又可分为组件和功能两个层次,技术又可分为基础平台与组件所需提供工件两个部分。
后续所有展示都围绕此二要素。
2.核心视图
由RUP贡献的四个视图是架构展示的核心视图。
逻辑视图(静态类图)
关注功能,不仅包括用户可见的功能,还包括为实现用户功能而必须提供的"辅助功能模块";它们可能是逻辑层、功能模块等。
应映射为业务组件、功能包以及技术工件(分层),以及它们之间关联依赖关系;
开发视图(静态类图)
关注程序包,不仅包括要编写的源程序,还包括可以直接使用的第三方SDK和现成框架、类库,以及开发的系统将运行于其上的系统软件或中间件。
开发视图和逻辑视图之间可能存在一定的映射关系:比如逻辑层一般会映射到多个程序包等。
应映射为具体的SDK和框架等,以及关联依赖关系;注:开发视图应尽可能和逻辑视图一一对应;
处理视图(动态类图)
关注进程、线程、对象等运行时概念,以及相关的并发、同步、通信等问题。
处理视图和开发视图的关系:开发视图一般偏重程序包在编译时期的静态依赖关系,而这些程序运行起来之后会表现为对象、线程、进程,处理视图比较关注的正是这些运行时单元的交互问题。
可映射为状态图和活动图(高层和详细);
物理视图(部署视图)
关注"目标程序及其依赖的运行库和系统软件"最终如何安装或部署到物理机器,以及如何部署机器和网络来配合软件系统的可靠性、可伸缩性等要求。
物理视图和处理视图的关系:处理视图特别关注目标程序的动态执行情况,而物理视图重视目标程序的静态位置问题;物理视图是综合考虑软件系统和整个IT系统相互影响的架构视图。
可映射为组件图,部署说明图;
3.扩展视图
在核心视图,针对于不同受众,还需要提供三个扩展视图。
非功能视图
展示非功能性指标的支持能力。通常针对于技术人员。
基础设施视图
展示架构所采用基础设施,以及它们之间的关系。通常针对于技术人员。
数据视图
关注于数据组织和存储形式。通常针对于DBA,或者需对数据进行定期维护的用户。
4.原则和约束
架构因明确说明本架构采用原则、方法论以及相关约束。
5.技术选型分析
由于架构涉及众多底层技术,也应给出相应的选型分析。