单用一篇文章很难把这个主题描述的清楚,但为了系列的完整性,笔者会围绕DDD中所介绍的内容做下初步总结,使读者有一个连续性。
一、概述
现在不是局部解决问题的时代了要运用新的技术创造新的效率提升,需要整个商业链条一起前进。需要以系统或生态的角度来考虑问题,而不能回到点到点的突破层面。
基于以上背景慢慢的会衍生出新的岗位和角色。技术层面也驱于两级化发展,初级开发越来越倾向于工具的使用,高级研发越来越需要更全面,进而对架构和架构师提出了额外的要求。
1.1、对架构师的要求
- 全局观:顶层设计+物理架构+应用架构,在顶层设计时充分考虑成本、效率、稳定、性能等因素;
- 基本要求:前瞻性和解决复杂问题的能力和思路;
- 能力要求:发现问题、定义问题、解决问题,问题是指一类问题;
1.2、一个良好的架构设计要包含
除了传统意义上所讲的CAP等两高或三高等,笔者认为更应该往下落一点,架构师不能只关心顶层设计,更多的要关心到细节或编码层面,这样才不至于出现实现偏差的问题。以下是笔者总结的几点:
- 清除单点:系统的每一层级都有不同的集群策略,同时还要考虑容器自动增删功能;
- 数据一致性:复杂业务可以采用消息中间件再配合不同级别的对帐机制保证;
- 强弱依赖:在成本可接受的情况下,还可以实现做一套备份流程来临时支撑系统运行,但会导致数据不一致,即一定时间窗口内数据不一致;
- 热点和极限值处理:a、隔离享用专有资源,适用于大客户的场景与普通客户分离;b、提前预热也可分为大小客户之分;c、简单逻辑、限流、排队等;d、IM这样的场景可以从业务上区分用户极别做到分极处理,在体验上有所侧重;
- 资损流程:这块的程序对于数据一致性的要求比较高,所以在设计时要额外增加对帐、总额控制、异常、风控等设计;
- 离线数据流:规模大、来源广、生产链路长,在整体生产和传输链路中,很容易出现数据少量丢失、部分环节失败、数据生产延迟等情况,最终消费在线系统也很难感知少量数据错误进而导致故障。针对离线数据流,要增加不同传输环节数据完整性校验、不同生产环节数据正确性校验、数据延迟监控、数据生产失败监控、端到端数据正确性规则校验、数据错误或延迟兜底预案、数据回滚重刷工具等机制;
- 异常流程设计:详细可参考笔者的另一篇文章 https://blog.51cto.com/arch/5295170 ;
二、SOLID
这个原则,是笔者认为比较好的一种指导思想,这里简单罗列一下概念,有兴趣的读者可深入研究一下,总结如下:
- 单一职责:单一原则的划分依据并不是根据实体来聚合的,这里是按职责或相互关联的功能来聚合的,职责可以从变化原因的角度来考虑;在落地时可以通过拆分来达到职责的单一;
- 开闭原则:在实际落地时一般采用抽象的理念,通过继承的方式来扩展功能,同时保证原代码的固化。但一个最重要的前提要考虑到多态的实现,这会直接影响函数的执行顺序;
- 里氏替换:落地时也是用了抽象和继承思想。它强调的是在不遵循开闭原则的前提下达到一种功能的增强和替换。如果子类不能完整的实现父类的方法,就应该采用依赖、聚合、组合等关系代替继承;
- 接口隔离:客户端不应该强迫依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。这个原则多少和单一职责有些关系,我们可以从安全或场景的角度来隔离接口;
- 依赖倒置:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。这个设计原则分了三层:高层-抽象-底层,可以被复用的是高层+抽象。而不是抽象;
三、常用的架构设计
常见的架构一般有经典MVC分层、六边形(又称接口适配器)、CQRS、事件驱动、微服务等,在DDD中主推的是CQRS、六边形、事件驱动三种。从落地情况来看多数会采用分层和六边形架构。
然而现实情况是我们很难去说清我们的系统到底用的是哪种架构模式,多数是一种混合架构,比如任一架构都存在层次、任一架构都可以存在多种服务协议接口。更多的是看如何来表述,架构的表述最重要的一点就是是否能描述清楚层次的职责以及不同层次之间的依赖关系和依赖原则(依赖细节还是依赖抽象)。下面是DDD的经典层次结构:
- UI表现层:显示信息、解释用户指令,用户指令解释;
- 用户接口层:提供给UI表现层的数据API接口;
- 应用接口层:定义软件要完成的任务,指挥领域层来解决问题,它不包含业务规则和状态,但需要管理安全和事务,但可以包含任务的进度状态;而只为下一层中的领域对象协调任务,分配工作,使它们互相协作,只提问;
- 领域层:也称模型层,控制业务状态,业务状态信息和业务规则,借助基础设施层持久化信息;只回答,内部通过适配器的方式与不同领域交互;
- 基础设施层:为上面各层提供通用的技术能力;相当于工具和持久层;持久化层的数据模型可以和领域模型完全不同,也可以相同。但要在概念上区分开;
3.1、MVC分层
3.1.1、设计原理
分层架构只有一个约束:上层只能与位于下方的层发生耦合。分为严格分层架构和松散分层架构,通常会采用松散分层架构,通常会遇到逆向调用的问题,一种解决方案是逻辑下沉,也可以采用以下两种方案实现逆向调用:
- 观察者模式:底层发布事件,高层接收;
- 调停者模式:底层定义接口,高层实现;同时可把实现做为参数向下传递;
- 图1:经典松散分层架构,需注意逆向调用的问题;
- 图2:主要解决领域层依赖基础设施的问题,它是把领域接口实现放在了应用层,由应用层去引用基础设施;
- 图3:采用依赖倒置的设计,通俗来讲就是:高层模块不依赖低层模块两者都依赖于抽象(接口定义),抽象不依赖细节(接口实现)而细节依赖抽象;
3.1.2、分层架构特殊说明
DDD中应用层一般是很薄的一层,不应该包含业务逻辑,一般用于控制持久化事务和安全认证,或者向其它系统发送消息通知,它主要用于接收端指令,再通过资源库获取聚合实例,然后执行相应的领域服务。
当领域层用于发布领域事件时,应用层可以将订阅方注册到任意数量的事件上,这样的好处是可以对事件进行存储和转发。这时领域层只需关注自己的核心逻辑即可,领域事件发布器也适合做成适配的方式,与底层基础设施间解耦(无论怎么设计领域层就是核心业务不能依赖基础设施层);
3.2、Adapter六边形
3.2.1、设计原理
从分层到六边形需要一个概念上的转换,端口和适配器的功能就是将客户输入转化成能被系统API所理解的参数,实际的操作是委托给内六边形的。这种架构中存在两种适配器:
- 处理输入:接收用户指令,向应用程序发起请求,调用port;
- 处理输出:响应请求,输出到外部设备或工具中,实现port;
六边形架构的优点是:
- 可以轻易开发用于测试的适配器;
- 应用程序和领域模型可以单独开发;
- 接入方也可以并行开发,通过适配器方式接入;
3.2.2、六边形架构特殊说明
适配器中的协议部分一般是由公共组件完成。在这种结构中,存储也被认为是一类客户。其存取服务在DDD是用资源库的方式实现。核心域也是通过适配器来与这个客户打交道的(即领域服务如果发布事件也要经过适配器来转发,即图上的颜色区域)。
六边形架构的边界是外六边形,在两个六边形之间应该有一个适配转发器的设计,来自动路由用户的请求到合适的适配器上,再由适配器请求应用程序。同时在适配器和应用程序之间存在一个职责交接的过程。我们也可以换一种图形来表述六边形架构:
3.3、CQRS事件驱动
3.3.1、设计原理
这种架构模式会带来系统的复杂性和基础依赖,但同时也能解决数据显示的复杂性问题,CQRS架构比较适合异构数据和需要读写分离的应用。概要设计如下图所示:
3.3.2、CQRS架构特殊说明
CQRS中存在两种模型,在代码实现时会把除了getById()以外的所有查询方法从原来的查询接口中分离出去;
- 查询模型:它不是领域模型的一部分只用于显示,设计查询模型时需要注意的是视图的个数;通常会在处理器中内置一个查询过滤器的功能用于减少视图的个数;在查询 时不能直接或间接是修改对象的状态;
- 命令处理器:专门负责更新对象的状态,当命令执行结束后,实例被更新同时必须发布一个领域事件更新查询模型。详细设计如下图所示:
四、工程设计
有了以上设计后,我们的源码目录一般也要做出相应调整,这一步非常重要因为会涉及到多人开发、打包、发布部署等工作,尤其在跨国时显的会尤为重要,本章是笔者在原来一家公司的落地经验可参考。 模块的设计体现在包路径的结构上,需要和具体的架构结合来设计,比如采用DDD设计,那么DDD从大的方面来讲就是三层:应用服务、领域服务、领域;这就是一层设计。而域内的服务、聚合、实体等可称为二级设计。如果和DDD结合那么建议模块由:
- 规范:固定顶级 + 上下文名称 + 1级层次(domain|[application/port.adapter]|repository) + [模块] + 2级层次(根据架构来设计service|model|event)
- 例子:com.xxx + refund + domain + submodule + service ,com.xxx.refund.domain.service,com.xxx.refund.domain.flowlog.service;
注意事项
- 不建议用通用的后缀名称,通用的后缀有可能也带来噪音和层次调用限制;而是用模块来区分;
- 第三层比如domain,这里面可能不包含任何代码;
附、清晰架构完整设计
附一张其它大牛的一张设计图