模块划分高层原则
-
以事件为切入点,以事件为落脚点。事件即扩展,对于每一个聚合模型来说,其内部产生的事件即该模型的扩展点,事件能够产生“涟漪”,事件内部保证事务一致性,事件之间通过补偿机制保证事务一致性,当然事件之间的一致性保证还有其它选择。
-
以聚合的上下文边界划分模块(服务),模块间严格进行逻辑隔离(至少实现逻辑隔离,物理隔离依赖分布式服务框架支持),这一个过程包括分析事件风暴,识别实体和聚合,根据聚合找到限界上下文,根据限界上下文划分模块,参考中汇公司CWAP架构规范,将聚合落地到具体模块,一个S工程或者一个U工程即一个模块。每个模块间严格确保数据隔离,例如用户模块需要提供获取用户详细的业务接口,但是为了满足不同的业务场景,可以定义若干个具体类型的接口,例如带共享缓存的接口,高速本地内存接口或者不带缓存的接口等。
-
模块内部必须是业务明确清晰的,只有从业务角度出发划分模块才能确保模块的清晰独立。一个聚合表达应该表达一个完整的业务领域,并且完整的存在于模块内部,也就是聚合封装了业务的绝大多数规则,聚合不应该跨模块(物理层面),聚合根内不允许注入仓储(Repository)、服务(Service)。
基于DDD的开发设计思想
新本币系统建设的核心目标是解决两大核心问题:
-
业务扩展问题。
-
系统性能问题,一个是查询的性能问题,一个是成交行情的性能问题。
针对复杂业务场景采用CQRS技术思想体系,针对简单业务场景采用MVC技术思想体系,CQRS的思想能够很好的解决两个核心问题。MVC思想更符合开发者思维习惯,简单暴力。例如多交易账户在新本币系统中起到基础支撑性作用,业务逻辑非常复杂,200页的需求文档已经无法安排一两个开发人员独立开发完成,所以必须采用CQRS体系;而对于报价档位行情设置这样的业务需求,则适合采用MVC体系开发。


模块划分具体实施步骤
- 准备输入文档:包含需求场景分析、需求文档和相关需求分析材料

- 采用Event Storming方式,根据业务场景和时间轴找到所有领域事件
识别和划分业务主要场景:
-
机构入市维护机构信息、用户信息和交易账户信息的场景
-
用户登录登出场景
-
交易过程关联多交易账户的场景
-
机构信息查询场景和用户信息查询场景

2 根据领域事件找到业务实体,并分析实体之间的关系,注意设计时应该反思需求的正确性,不能所有设计都是围绕需求来,如果聚合非常简单,原则上不需要单独出图。

- 分析聚合,找到落地模块,如果聚合业务比较独立,则可能不需要单独出图。

4针对每一个模块进行具体的落地设计,避免开发商开发人员走偏。设计内容主要包括聚合根、实体和值对象,以及领域事件、业务场景和对应的需求条目和业务规则。

聚合调用规范
聚合之间不可以直接相互调用。聚合根之间如果相互引用,则会造成一个可怕的后果,那就是:很容易导致取出一个聚合时会级联取出很多直接或间接引用到的其他聚合根,到最后可能会取出整个对象树。
聚合之间调用有三种方式:
-
聚合只存储另一个聚合的聚合根的ID。但会增加模型的复杂性。聚合根之间通过ID方式引用,而不是通过指针引用。ID同样可以起到表示对象关系的作用;使用ID关联可以天生让聚合更轻巧,节省不必要的内存,提高性能和可伸缩性;使用ID关联可以避免取出一个聚合时,整个数据库被拖出来的风险,当然这是在没有LazyLoad支持的情况下才会发生; 使用ID关联的聚合不会对ORM等持久化机制有特殊要求,比如必须支持LazyLoad特性等;ID是值对象,具有不变性,而引用则不是。
-
引入领域服务的概念,面向过程的编排让领域服务来完成多个聚合根之间的通信。领域服务知道该如何以面向过程的方式如何先调用第一个聚合根做事情,然后再调用第二个聚合根做事情,以此类推。
-
可以通过领域事件实现Don't Ask, Tell。即在聚合中如果做了什么操作,本来该调用其他聚合根做事情的地方触发一个领域事件出来,然后其他的领域对象监听该事件,从而完成对象之间的通信。通过这种方法,我们可以在整个领域模型中减少很多领域服务。
优先推荐第3种方案,扩展性最好,业务逻辑变更时,可以只是增加或者减少事件监听器。第一种方案的难度,但是相比第二个方案有更好的扩展性,建议优先采用领域事件实现扩展,其次是通过引入被调用的聚合根ID作为值对象,最后采用领域服务的概念解决聚合调用问题。
聚合设计规范
聚合的设计正是真正面型对象OO的设计,要求将数据和行为封装在对象内部,或者说是聚合内部,聚合对外部只暴露聚合根,因此要求数据的必须确保封装在聚合内部。例如成交明细处理需求中要获取用户的详细信息,只能通过用户的聚合根据用户的ID获取用户的详细信息,不可以直接通过成交有关的数据表与用户有关的数据表关联(表关联方式)获取用户的信息,这样破坏了聚合设计的初衷,为后期业务扩展和系统维护阶段埋下隐患。
一个聚合包含聚合根(Aggregate Root)、实体(Entity)和值对象(Value Object)三个核心概念以及领域模型在运行过程中产生的各种领域事件(Domain Event)。待tbs-s-cqrse(ddd)工程完成之后,我们会为所有模型提供统一的领域模型运行底座,以保证大家的代码编排更加合理统一,无歧义,尤其是确保领域事件能够真正发挥其支持业务扩展的力量。
下面以Java的jar为例示意,具体实践可以参考交易基础平台的tbs-s-account工程。
tbs-s-user
--domain (放置核心领域模型对象)
--events (放置领域事件以及事件监听处理器)
--commands (放置命令事件以及命令事件处理器)
--repository(放置有关领域模型持久化的仓库)
--service(放置有关查询报表业务使用的数据库操作接口和实现,接口一般来说没有必要)
--exception (放置针对该业务领域定义的业务异常)
--rest(放置模块对应场务端和统一终端的REST控制器)
XXXComponent.java(组件声明和初始化加载类)
XXXHelper.java(辅助类)
XXXContext.java(上下文容器)
模块间API设计规范
新本币系统内部一定有很多模块。模块间原则上只有接口交互,不存在数据库或者分布式共享缓存或者文件系统交互。如果需要针对某一块功能添加缓存,那么缓存一定是在接口之后,被接口屏蔽的,最好不要在接口之前添加缓存。在接口之前加缓存存在如下问题:
-
一个接口被多个功能服务调用,就意味着每个调用者都要自己实现缓存机制,重复建设,当数据变更之时,清理缓存机制设计变得更加复杂,不可靠。
-
调用者实现缓存不利于模块自身的功能完善,尤其是不利于推进模块面向领域演进。
-
调用者添加缓存将增加模块之间的耦合度,不利于后期扩展和维护。
模块间接口类必须以XXXApi命名,并继承CWAP框架的com.cfets.cwap.s.spi.Provider这个接口,以表达该类承担了服务提供者的职责。调用模块通过Spring的@Resource或者@Autowired注入服务接口,接口的使用最好在组件的XXXComponent类初始化完成时注入组件的XXXContext类中,以备内部模块使用,对于多模块组件化架构来说,非常关注组件与接口的关系,这样在一个接口变更之时,能够通过技术手段找到所有有关该接口的运用组件,以减少需求变更之时对于设计与开发的评估成本。

对于成交行情和报价行情对性能要求极高的主业务模块,原则上必须调用用户模块、机构模块、授信模块提供的带缓存的高能力服务接口(High Api),避免被调用模块间接与数据库交互,拖累主业务模块的性能。最后注意Redis缓存对性能的优化是有限的,对于经常不变的基础数据例如机构和节假日数据最好的保存方式是本地内存缓存。
参考文献
Axon Framework Reference Guide
事件统一框架和持续开发体系https://www.jianshu.com/p/c81ae702ee21
网友评论