CQRS
CQRS(Command and Query Responsibility Segregation)命令查询职责分离模式,分别对读和写建模。
CQRS从定义上要求:
一个方法修改了对象的状态,该方法便是一个Command,它不应该返回数据。
一个方法返回了数据,该方法便是一个Query,此时它不应该通过直接的或间接的手段修改对象的状态。
CQRS 适用于场景
极少数复杂的业务领域,如果不是很适合反而会增加复杂度
为获取高性能的服务
单独设计每个投影,并使用它们自己的数据存储,而不要多个投影共用一个巨大的数据存储,互相影响
六边形架构
六边形架构(Hexagonal Architecture)又称为端口和适配器风格,为了突显这是个扁平的架构,每个边界的权重是相等的。
经典分层架构分为三层(展现层、应用层、数据访问层),而对于六边形架构,可以分成另外的三层:
领域层(Domain Layer):最里面,纯粹的核心业务逻辑,一般不包含任何技术实现或引用。
端口层(Ports Layer):领域层之外,负责接收与用例相关的所有请求,这些请求负责在领域层中协调工作。端口层在端口内部作为领域层的边界,在端口外部则扮演了外部实体的角色。
适配器层(Adapters Layer):端口层之外,负责以某种格式接收输入、及产生输出。比如,对于 HTTP 用户请求,适配器会将转换为对领域层的调用,并将领域层传回的响应进行封送,通过 HTTP 传回调用客户端。
六边形架构的优点:使业务边界更加清晰,从而获得更好的扩展性,业务复杂度和技术复杂度分离也是 DDD 的重要基础。
DDD设计
理解业务
领域驱动设计是对业务模型在系统设计中的一种表现形式,在进行DDD实战前一定要熟悉业务,不熟悉业务无法把业务模型翻译成域模型。理解业务-站在业务方和产品角度,梳理系统业务的所有细节,明白每一个业务细节点。
领域模型的特点
领域模型是现实世界中对象的可视化表示;
领域模型是分析需求阶段的产物,是表述问题空间的模型,用于指导后续的系统设计;
在设计领域模型时不应该考虑后续具体的系统实现;
领域建模的意义
通过建立领域模型的过程不断拉齐团队内成员对需求的认知
随着真实需求的不断清晰,再反过来持续校准领域模型,使得领域模型能更好的反映真实世界
领域模型建立过程中,团队成员会在有界的上下文中有意识的形成统一语言,便于后续的沟通
我们裂变业务现状存在的两个问题 *业务分散在各处,独立维护,业务概念和模型互相独立五花八门,项目组人员对这些业务的流程并不是那么熟悉
我们裂变业务现在仅仅只为单业务服务,模型大都缺乏前瞻性和扩展性,仅对自身裂变业务的模型做简单抽象得到的模型,并不能满足裂变玩法平台化的要求
针对这两个问题的应对方案:重新对裂变玩法进行领域建模,目标是产出一套新的统一语言以及一套足够稳健的领域模型,能够有效的支持现有业务及未来出现的类似的新业务
裂变玩法领域建模的意义
通过建立裂变玩法问题域领域模型的过程不断拉齐团队内成员对这块业务的认知
通过建模的过程不断挖掘准入裂变玩法的真实需求,持续校准领域模型,使得领域模型能更好的反映真实世界,也具备一定的前瞻性和扩展性
业务抽象
在梳理业务过程中会把业务每一个具体的点都给罗列出来,是一盘零碎的业务点。通过对业务的理解进行抽象,把相关性的业务点进行分组聚合。业务抽象过程中涉及边界划分问题
在建模过程中增加业务方,运营,客服等非技术性的人员的参与度,他们往往能补充技术人员对业务的盲点
模型翻译
在经过业务抽象之后,整体业务模型已经清晰明了,在域模型设计上,只需要把业务模型经过简单翻译映射成域模型即可。
子域划分
业务模型翻译成域模型后,当一个域模型比较复杂的时候需要把一个域模型进行子域划分
其它
Entity与持久层的DO异构
贫血模型下,VO与DO的区别是很小的,基本是简单的值对应关系。通常我们使用各种copyProperties()、convertDOToVO()等操作就可以完成转换
充血模型下,entity都是深层次嵌套对象,对于持久层DO转换Entity和Entity转换DO都是需要额外编写工作量不小的转换代码的。推荐使用开源工具库 MapStruct,本质是一个转换代码生成工具,提供编译时生成转换代码。
设计规范
Entity设计规范
Entity最重要的设计原则是保证实体的不变性(Invariants),也就是说要确保无论外部怎么操作,一个实体内部的属性都不能出现相互冲突,状态不一致的情况。
原则1:创建即一致
constructor参数要包含所有必要属性,或者在constructor里有合理的默认值。
使用Factory模式来降低调用方复杂度
原则2:尽量避免public setter
因为set单一参数会导致状态不一致的情况;
@ Setter(AccessLevel.PRIVATE) // 确保不生成public setter
原则3:通过聚合根保证主子实体的一致性,主实体会包含子实体,主实体就起到聚合根的作用,即:
子实体不能单独存在,只能通过聚合根的方法获取到。任何外部的对象都不能直接保留子实体的引用
子实体没有独立的Repository,不可以单独保存和取出,必须要通过聚合根的Repository实例化
子实体可以单独修改自身状态,但是多个子实体之间的状态一致性需要聚合根来保障
原则4:不可以强依赖其他聚合根实体或领域服务。对外部对象的依赖性会直接导致实体无法被单测;以及一个实体无法保证外部实体变更后不会影响本实体的一致性和正确性。正确的对外部依赖的方法有两种:
只保存外部实体的ID:强烈建议使用强类型的ID对象,而不是Long型ID。强类型的ID对象不单单能自我包含验证代码,保证ID值的正确性,同时还能确保各种入参不会因为参数顺序变化而出bug。
针对于“无副作用”的外部依赖,通过方法入参的方式传入。
原则5:任何实体的行为只能直接影响到本实体(和其子实体)
原则6:实体的充血模型不包含持久化逻辑
Domain Service 设计规范
领域服务一般分为单对象策略型、跨对象事务型、通用组件型三种。
单对象策略型:主要面向的是单个实体对象的变更,但涉及到多个领域对象或外部依赖的一些规则。实体应该通过方法入参的方式传入这种领域服务,然后通过Double Dispatch来反转调用领域服务的方法。
跨对象事务型:当一个行为会直接修改多个实体时,不能再通过单一实体的方法作处理,而必须直接使用领域服务的方法来做操作。
通用组件型:与ECS里的System类似,提供了组件化的行为,但本身又不直接绑死在一种实体类上。
让Domain Service与Repository打交道,而不是让领域模型Entity与Repository打交道,因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository)或开发框架(比如Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从DB中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。
Domain Service类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的RPC接口等,都可以放到Domain Service类中。
#### Application Service 设计规范
Application Service 是业务流程的封装,不处理业务逻辑,即不要有if/else分支逻辑、不要有任何计算、一些数据的转化可以交给其他对象来做。
常用的ApplicationService“套路”:
准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。
执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。
持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。
其它规范
Interface层:
职责:主要负责承接网络协议的转化、Session管理等
接口数量:避免所谓的统一API,不必人为限制接口类的数量,每个/每类业务对应一套接口即可,接口参数应该符合业务需求,避免大而全的入参
接口出参:统一返回Result
异常处理:应该捕捉所有异常,避免异常信息的泄漏。可以通过AOP统一处理,避免代码里有大量重复代码。
Application层:
入参:具像化Command、Query、Event对象作为ApplicationService的入参,唯一可以的例外是单ID查询的场景。
CQE的语意化:CQE对象有语意,不同用例之间语意不同,即使参数一样也要避免复用。
入参校验:基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP,也可以自己写AOP。
出参:统一返回DTO,而不是Entity或DO。
DTO转化:用DTO Assembler负责Entity/VO到DTO的转化。
异常处理:不统一捕捉异常,可以随意抛异常。
部分Infra层:
用ACL防腐层将外部依赖转化为内部代码,隔离外部的影响
领域事件
一般的副作用发生在核心领域模型状态变更后,同步或者异步对另一个对象的影响或行为。副作用的处理方法—领域事件
领域事件介绍
领域事件是一个在领域里发生了某些事后,希望领域里其他对象能够感知到的通知机制。
领域事件将隐性的副作用“显性化”,通过一个显性的事件,将事件触发和事件处理解耦,最终起到代码更清晰、扩展性更好的目的。
领域事件实现
领域事件通常是立即执行的、在同一个进程内、可能是同步或异步。可通过一个EventBus来实现进程内的通知机制。
缺陷:领域事件的很好的实施依赖EventBus、Dispatcher、Invoker这些属于框架级别的支持。但因为Entity不能直接依赖外部对象,所以EventBus目前只能是一个全局的Singleton,导致Entity对象无法被完整单测覆盖全。
领域建模
从认识的角度来理解建模建模
保证领域模型与需求严格同步:
一个简单的规则:任何在需求描述中出现的概念,都必须出现的领域模型中,如果需求描述中存在概念之间的关系,领域模型中也必须有这个关系。
微服务架构中10个常用的设计模式
1 微服务架构
微服务架构的重要特征
微服务架构的优点
微服务架构的缺点
何时使用微服务架构
2 微服务架构的设计模式
独享数据库(Database per Microservice)
事件源(Event Sourcing)
命令和查询职责分离(CQRS)
Saga
面向前端的后端 (BFF)
API 网关
Strangler
断路器
外部化配置
消费端驱动的契约测试
3 总结
从软件开发早期(1960 年代)开始,应对大型软件系统中的复杂性一直是一项令人生畏的任务。多年来为了应对软件系统的复杂性,软件工程师和架构师们做了许多尝试:David Parnas 的模块化和封装 (1972), Edsger W. Dijkstra (1974)的关注点分离以及 SOA(1988)。
他们都是使用分而治之这项成熟的传统技术来应对大型系统的复杂性。自 2010 年开始,这些技术被证实无法继续应对 Web 级应用或者现代大型企业级应用的复杂性。因此架构师和工程师们发展出了一种全新的现代方式来解决这个问题,就是微服务架构。它虽然延续了分而治之的思想,但却是以全新的方式来实现的。
软件设计模式是解决软件设计中常见问题的通用、可复用的解决方案。设计模式让我们可以分享通用词汇并使用经实战检验的方案,以免重复造轮子。我先简单介绍下微服务架构。
通过阅读这篇文章,你会学到:
微服务架构
微服务架构的优势
微服务架构的劣势
何时使用微服务架构
最重要的微服务架构设计模式,包括其优缺点、用例、上下文、技术栈示例及可用资源。
请注意,本清单中的大部分设计模式常出现在多种语境中,并且可以在非微服务架构中使用。而我将在微服务这个特定语境中介绍它们。