以前写过一篇《基于抽象的分层结构》,这里补充一篇《基于业务模块组件的系统架构》
一些内容在《项目笔记:dao,web,模块边界以及Model分类》以及《模块的接口设计》随笔中已经提到,这里补充总结一下。
任何一个有一定规模系统,通常会把系统做一定分解降低分析设计开发的难度,模块划分是一个比较常见的方式。
而在模块的划分及其分析设计的实践中,包括了两种层次的边界。第一是交互行为层次,第二是对象层次。
首先说交互行为。
模块和模块的交互接口最为重要,通常我们认为这些接口应该通用稳定,然而如何设计每个模块对外提供的接口却是一个不易的问题。
集成的两种基本实现方式:
1. API方式。
这是常见的实践方式。即每个模块公开数量不多的接口类(但拥有较多的接口方法)以及接口参数(都是普通的VO),每个接口的设计和实现都有该模块成员维护;实践中:这些接口都独立打包,形成多(模块)对一(接口)的依赖关系,方便编译。
这样的好处是:接口边界清晰,开发编译依赖比较简单。
然而实践中极有可能出现两种状况:接口维护失控或者过严而死板(而影响开发)。接口失控是因为接口的维护太过随意,因为A模块的需要就轻易在B模块中添加一个接口(方法),导致该接口(方法)非独立性(基本上只给模块A的这个功能点使用),或者是接口的控制过严,导致或者工作效率不高,或者接口的易用性不好。
原因在于:接口是两个模块间的耦合,而发生的种种问题在于模块耦合太过紧密;同时实践中,把模块对外提供的接口,与模块需要实现的外部模块的集成混为一谈;换句话说,A模块需要集成的语义,和B模块提供接口的语义存在差距。
这样的实践优点:开发管理控制成本较低,模块编译依赖简单,模块物理边界明确。
2. Adapter方式
这种的实践可以一定程度上解决API方式面临的问题:根据指导原则——为了降低耦合只有在中间加一层;即不轻易为模块设计对外提供的接口(方法),除非是通过重构得来的;模块对外提供两种类:一个是需要外部模块实现的接口(接口设计从本模块需要出发,当然每个接口尽管是为某个功能点服务,但也要注意其在模块内通用性);另一个是其它模块要求本模块实现的接口的实现类(这块物理上独立打包,独立于模块之外)。
即:A模块拥有一些需要B模块实现的接口(A模块对B模块的要求),而B模块中也有要求A模块实现的接口,因而A有这些接口的实现类。
处于编译依赖的考虑,已有的实践是把处理接口适配的代码维护在模块外的,就是所谓的集成模块,这样编译上就形成了1(接口实现)对n(模块)的依赖关系。模块的边界在这里模糊了,当然在这之外模块的边界是很清晰的。
这种实践方式的好处在于:模块的接口就多了一层隔离降低了耦合,把接口的通用性和接口的适应性分离,又明确了模块的边界,使得接口在日后的优化和调整有了缓冲。
这样的实践和API实践的不同之处在于:接口的设计权利一分为二,需求方拥有了其应该有的权利,并且这个权利与提供方没有冲突(旧的模式不行,一旦供应方改变了接口,就立刻导致编译问题)。
两种方式的对比:
1. 采用API方式集成,代价在于维护API本身,API面对众多的客户端,其面临的设计维护成本较高,同时实施成本高;
2. 采用Adapter方式集成,由于没有维护API,缺乏可复用接口,但可以做到为每个客户端自由定制,其设计维护成本较低,实施上成本较高;
集成的演进方式:
1. 采用事件模式。
这也是完成交互处理的模式。通常事件模式在业务行为上也可以完成和接口相互的功能,不过由于其在接口签名上无法明确提供其业务含义,建议谨慎应用。对于产品化项目,项目开始可以应用事件处理机制,在成熟时提取为更具体的接口。
事件模式中,Event的监听类也面临着模块的边界问题。已知的成熟开源项目都在事件源中暴露的模块内部对象,方便开发者使用。
可以说这种方式的是Adapter方式的演进;
2. 采用框架回调方式
工作流和页面流也负责集成不同的模块.不过因为工作流和页面流框架都维护独立的数据对象共享池,因而在这个层面上,模块间直接的交互并不存在。这种集成上的优势,演化成通过ESB等集成框架完成.
这种方式是API方式的演进,但是把调用点以及调用方独立出来,成为框架的一部分。
现在说说对象一级的边界集成。
在保险中,benefit对象会关联一个product对象,不过product对象是是属于产品模块而非保单模块,对于保单模块只关心id而并不使用对象,但在RMS或者FMS或者UIC这样的集成环境却是需要product对象的。我们要采用代码生成的方式,在benefit对象上加一个annotation,比如Integration的annotation标识,使用AspectJ在编译上enhancement生成的class,使得benefit对象在集成环境上可以拿到product对象,这算是一个集成方面。
组件化管理
组件化管理包含两个含义:
1. 组件访问控制,依赖管理以及服务注册;
现有的访问控制的办法,只能通过代码提交时或者编译发布时检查,但我们更期望支持运行时的能力,避免外部组件访问组件未申明公开的内部结构和程序;同时维护方式应该简单方便,成本低。
目前的组件依赖管理几乎没有,尤其是基于版本的依赖;而在实践中,依赖维护管理尤其是多版本的通常花费大量的成本。
2. 组件依赖的绑定以及失效(故障)的隔离;
我们希望能够为每个组件提供一个保护区域,当其依赖的一个组件因为种种原因(软件错误的或者软件升级)失效时,系统至少应该提供一种fail-fast机制,包括如下能力:
A.故障即停(Halt on failure)——当一个组件出错时,应当立即停止下来,而不是继续执行可能不正确的操作;
B.故障曝光性质(Failure status property)——当一个组件发生故障时,系统中的其他组件应该得到通知,故障的原因必须交代清楚;
同时,我们希望该组件,如果可能它应该可以继续运行。完成这样的目标需要很多的工作,但首先依赖于系统fail-fast能力,同时在故障消除服务恢复后,系统能够提供相应的通知,并控制恢复的顺序。
已知的支撑框架:SCA和OSGi
1. SCA考虑的是异构异步环境,OSGi考虑同构同步环境;
2. OSGi规定了容器如何管理组件,组件依赖(版本)管理,组件边界保护以及组件服务依赖运行时绑定策略和权限控制;SCA对此没有涉及;
3. SCA考虑的如何暴露服务,设计时客户端如何使用服务;OSGi考虑的更加完善;