【DDD】学习笔记-发布者—订阅者模式

news2024/9/20 22:58:18

在领域设计模型中引入了领域事件,并不意味着就采用了领域事件建模范式,此时的领域事件仅仅作为一种架构或设计模式而已,属于领域设计模型的设计要素。在领域设计建模阶段,如何选择和设计领域事件,存在不同的模式,主要为发布者—订阅者模式和事件溯源模式,它们可以统称为“领域事件模式”。

发布者—订阅者模式

发布者—订阅者(Publisher-Subscriber)模式严格说来是一种架构模式,在领域驱动设计中,它通常用于限界上下文(或微服务)之间的通信与协作。为表区分,在领域模型内部使用事件进行状态通知的模式属于观察者模式,不属于发布者—订阅者的范畴。

由于事件消息无需返回值,就使得事件的发布可以采用异步非阻塞模式,因此,采用事件的发布者—订阅者模式不仅能够解除限界上下文之间的耦合,还能提高系统的响应能力。如今,基于流的响应式编程也越来越成熟,如 Kafka 这样的消息中间件通常又具有极强的吞吐能力和水平伸缩的集群能力,使得消息能够以接近实时的性能得到处理。

当我们采用发布/订阅事件来处理限界上下文之间的通信时,要明确限界上下文的边界,进而决定事件消息传递的方式。如果相互通信的限界上下文处于同一个进程内,就要考虑:引入一个分布式的消息中间件究竟值不值得?分布式通信可能会带来事务一致性、网络可靠性等多方面的问题,与其如此,不如放弃选择发布者—订阅者模式,改为观察者模式,又或者放弃分布式的消息中间件,选择共享内存的事件总线,如采用本地 Actor 模式,由 Actor 对象内置的 MailBox 作为传输事件的本地总线,达到异步通信(非跨进程)的目的。

应用事件

如果选择分布式的消息中间件实现发布者—订阅者模式,则限界上下文之间传递的领域事件属于外部事件。与之相对的是内部事件,它包含在限界上下文内的领域模型中。既然外部事件用于限界上下文之间,就应该由应用层的应用服务来负责发布生成和发布事件。由于外部事件和内部事件的定义过于含糊,考虑到这些事件所处的层次和边界,我将外部事件称之为“应用事件”,内部事件则保留为“领域事件”的名称,这样恰好可以与分层架构的应用层、领域层相对应。

应用事件与领域事件的作用不同。应用事件通常用于限界上下文之间的协作,由应用服务来负责,如果限界上下文的边界为进程边界,还需要考虑跨进程的事件消息通信。应用事件采用的模式为发布者—订阅者模式。领域事件属于领域模型的一部分,如果用于限界上下文内部之间的协作,采用的模式为观察者模式;如果领域事件表达的是状态迁移,采用的模式为事件溯源模式。发布一个领域事件就和创建一个领域对象一样,都是内存中的操作。只是在持久化时,才需要访问外部的资源。

如果一个事件既需要当前限界上下文关心,又需要跨限界上下文关心,那么,该事件就相同于同时扮演了领域事件和应用事件的角色。由于应用层依赖于领域层,即使是定义在领域层内部的领域事件,应用层也可以重用它。如果希望隔离外部限界上下文对领域事件的依赖,也可以将该领域事件转换为应用事件。

应用事件作为协调限界上下文之间的协作消息,存在两种不同的定义风格,Martin Fowler 将其分别命名为:事件通知(Event Notification)和事件携带状态迁移(Event-Carried State Transfer)。注意,这两种风格在发布者—订阅者模式中,起到都是“触发器”的作用。但两种风格的设计思维却如针尖对麦芒,前者降低了耦合,却牺牲了限界上下文的自治性;后者恰好相反,在换来限界上下文的自治性的同时,却是以模型耦合为代价的。

说明:Martin Fowler 在其文章 What do you mean by “Event-Driven”? 中探讨了所谓“事件驱动”的模式,除了上述的两种模式之外,还有事件溯源与 CQRS 模式。但我认为前两种模式属于事件消息定义风格,主要用于发布者—订阅者模式。发布者—订阅者模式与 CQRS 模式同属于架构模式,而事件溯源则属于领域模型的设计模式。

由于应用事件要跨越限界上下文,倘若事件携带了当前限界上下文的领域模型对象,在分布式架构中,订阅方就需要定义同等的包含了领域模型对象的应用事件。一旦应用事件携带的领域模型发生了变化,发布者与订阅者双方都要受到影响。为了避免这一问题,应用事件除了包含消息通知所必须具备的属性之外,不要传递整个领域模型对象,仅需携带该领域模型对象的身份标识(ID)。这就是所谓的“事件通知”风格。

由于“事件通知”风格传递的应用事件是不完整的,倘若订阅方需要进一步知道该领域模型对象的更多属性,就需要通过 ID 调用发布方公开的远程服务去获取。服务的调用又为限界上下文引入了复杂的协作关系,反过来破坏了事件带来的松散耦合。倘若将应用事件定义为一个相对自给自足的对象,就可以规避这些不必要的服务协作,提高了限界上下文的独立性。这就是“事件携带状态迁移”风格。

“事件携带状态迁移”风格要求应用事件携带状态,就可能需要在事件内部内嵌领域模型,导致发布方与订阅方都需要重复定义领域模型。为避免重复,可以考虑引入共享内核来抽取公共的应用事件类,然后由发布者与订阅者所在的限界上下文共享。若希望降低领域模型带来的影响,也可以尽量保持应用事件的扁平结构,即将领域模型的属性数据定义为语言框架的内建类型。如此一来,发布者与订阅者双方只需共享同一个应用事件结构即可,当然坏处是需要引入从领域模型到应用事件的转换。

一个定义良好的应用事件应具备如下特征:

  • 事件属性应以内建类型为主,保证事件的平台中立性,减少甚至消除对领域模型的依赖
  • 发布者的聚合ID作为构成应用事件的主要内容
  • 保证应用事件属性的最小集
  • 为应用事件定义版本号,支持对应用事件的版本管理
  • 为应用事件定义唯一的身份标识
  • 为应用事件定义创建时间戳,支持对事件的按序处理
  • 应用事件应是不变的对象

我们可以为应用事件定义一个抽象父类:

public class ApplicationEvent implements Serializable {
    protected final String eventId;
    protected final String createdTimestamp;
    protected final String version;

    public ApplicationEvent() {
        this("v1.0");
    }

    public ApplicationEvent(String version) {
        eventId = UUID.randomUUID().toString();
        createdTimestamp = new Timestamp(new Date().getTime()).toString();
        this.version = version;
    }  
}

在业务流程中,我们经常面对存在两种操作结果的应用事件。不同的结果会导致不同的执行分支,响应事件的方式也有所不同。定义这样的应用事件也存在两种不同的形式。一种形式是将操作结果作为应用事件携带的值,例如支付完成事件:

public enum OperationResult {
    SUCCESS = 0, FAILURE = 1
}

public class PaymentCompleted extends ApplicationEvent {
    private final String orderId;
    private final OperationResult paymentResult;

    public PaymentCompleted(String orderId, OperationResult  paymentResult) {
        super();
        this.orderId = orderId;
        this.paymentResult = paymentResult;
    }
}

采用这一定义的好处在于可以减少事件的个数。由于事件自身没有体现具体的语义,事件订阅者就需要根据 OperationResult 的值做分支判断。若要保证订阅者代码的简洁性,可以采用第二种形式,即通过事件类型直接表现操作的结果:

public class PaymentSucceeded extends ApplicationEvent {
    private final String orderId;

    public PaymentSucceeded (String orderId) {
        super();
        this.orderId = orderId;
    }
}

public class PaymentFailed extends ApplicationEvent {
    private final String orderId;

    public PaymentFailed (String orderId) {
        super();
        this.orderId = orderId;
    }
}

这两个事件定义的属性完全相同,区别仅在于应用事件的类型。

微服务的协同模式

若将限界上下文视为微服务,则发布者—订阅者模式遵循了协同(Choreography)模式来处理彼此之间的协作,这就决定了参与协作的各个限界上下文地位相同,并无主次之分。由于事件消息属于异步通信模式,因此在运用发布者—订阅者模式时,需要结合业务场景,明确哪些操作需要引入应用事件,由谁发布和订阅应用事件。发布者—订阅者模式并非排他性的模式,例如在执行查询操作时,又或者执行的命令操作并不要求高响应能力时,亦可采用同步的开放主机服务模式。

若要追求微服务架构的一致性,保证微服务自身的自治性,可考虑在架构层面采用纯粹的事件驱动架构(Event-Driven Architecture,EDA)。遵循事件驱动架构,微服务之间的协作皆采用异步的事件通信模式。即使协作方式为查询操作,也可使用事件流在服务本地缓存数据集,从而保证在执行查询操作时仅需要执行本地查询即可。要支持本地查询,需要在每次发布事件时,对应的订阅者负责获取自己感兴趣的数据,并将其缓存到本地服务的存储库中。例如,下订单场景需要订单服务调用库存查询服务以验证商品是否满足库存条件。若要避免跨服务之间的同步查询操作,就需要订单服务事先订阅库存事件流,并将该库存事件流保存在订单服务的本地数据库中。库存服务的每次变更都会发布事件,订单服务会订阅该事件,然后将其同步到库存事件流,以保证订单服务缓存的库存事件流是最新的。

既然限界上下文的协作方式发生了变化,意味着应用服务之间的调用方式也将随之改变。

在买家下订单的业务场景中,考虑订单上下文与支付上下文之间的协作关系。如果采用开放主机模式,则订单上下文将作为下游发起对支付服务的调用。支付成功后,订单状态被修改为“已支付”,按照流程就需要发送邮件通知买家订单已创建成功,同时通知卖家发货。这时,订单上下文会作为下游发起对通知服务的调用。显然,在这个业务场景中,订单上下文成为了整个协作过程的“枢纽站”:

70835542.png

发布者—订阅者模式就完全不同了。限界上下文成为了真正意义上的自治单元,它根本不用理会其他限界上下文。它像一头敏捷的猎豹一般游走在自己的领土疆域内,凝神静听,伺机而动,一旦自己关心的事件发布,就迅猛地将事件“叼”走,然后利用自己的业务逻辑去“消化”它,并在满足业务条件的时候,发布自己的事件“感言”,至于会是谁对自己发布的事件感兴趣,就不在它的考虑范围内了。显然,采用事件风格设计的限界上下文都是各扫门前雪,彼此具有平等的地位:

79256322.png

订单上下文既订阅了支付上下文发布的 PaymentCompleted 事件,又会在更新订单状态之后,发布 OrderPaid 事件。假定我们选择 Kafka 作为消息中间件,就可以在订单上下文定义一个事件订阅者,侦听指定主题的事件消息。该事件订阅器是当前限界上下文的北向网关:

public class PaymentEventSubscriber {
    private ApplicationEventHandler eventHandler;

    @KafkaListener(id = "payment", clientIdPrefix = "payment", topics = {"topic.ecommerce.payment"}, containerFactory = "containerFactory")
    public void subscribeEvent(String eventData) { 
        ApplicationEvent event = json.deserialize<PaymentCompleted>(eventData);
        eventHandler.handle(event);
    }
}

ApplicationEventHandler 是一个接口,凡是需要处理事件的应用服务都可以实现它。例如 OrderAppService:

public class OrderAppService implements ApplicationEventHandler {
    private UpdatingOrderStatusService updatingService;
    private ApplicationEventPublisher eventPublisher;

    public void handle(ApplicationEvent event) {
        if (event instanceOf PaymentCompleted) {
            onPaymentCompleted((PaymentCompleted)event);
        } else {...}
    }

    private void onPaymentCompleted(PaymentCompleted paymentEvent) {
        if (paymentEvent.OperationResult == OperationResult.SUCCESS) {
            updatingSerivce.execute(OrderStatus.PAID);          
            ApplicationEvent orderPaid = composeOrderPaidEvent(paymentEvent.orderId());      
            eventPublisher.publishEvent(“payment", orderPaid);
        } else {...}
    }
}

OrderAppService 应用服务通过 ApplicationEventPublisher 发布事件。这是一个抽象接口,扮演了南向网关的作用,它的实现属于基础设施层,依赖了 Kafka 提供的 kafka-client 框架,通过调用该框架定义的 KafkaTemplate 发布应用事件:

public class ApplicationEventKafkaProducer implements ApplicaitonEventPublisher {
    private KafkaTemplate<String, String> kafkaTemplate;

    public void publishEvent(String topic, ApplicationEvent event) {
        kafkaTemplate.send(topic, json.serialize(event);
    }
}

采用发布者—订阅者模式实现限界上下文之间的协作时,要注意应用层对领域逻辑的保护与控制,确保领域逻辑的纯粹性。领域层的领域模型对象并未包含应用事件。应用事件属于应用层,类似服务调用的数据契约对象。事件的订阅与发布属于基础设施层:前者属于北向网关,可以直接依赖消息中间件提供的基础设施;后者属于南向网关,应用服务需要调用它,为满足整洁架构要求,需要对其进行抽象,再通过依赖注入到应用服务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1464877.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通信入门系列——锁相环、平方环、Costas环

微信公众号上线&#xff0c;搜索公众号小灰灰的FPGA,关注可获取相关源码&#xff0c;定期更新有关FPGA的项目以及开源项目源码&#xff0c;包括但不限于各类检测芯片驱动、低速接口驱动、高速接口驱动、数据信号处理、图像处理以及AXI总线等 本节目录 一、锁相环 1、压控振荡…

探索分布式强一致性奥秘:Paxos共识算法的精妙之旅

提到分布式算法&#xff0c;就不得不提 Paxos 算法&#xff0c;在过去几十年里&#xff0c;它基本上是分布式共识的代名词&#xff0c;因为当前一批常用的共识算法都是基于它改进的。比如&#xff0c;Fast Paxos 算法、Cheap Paxos、Raft 算法等。 由莱斯利兰伯特&#xff08;L…

AI Agent深入浅出——以ERNIE SDK和多工具智能编排为例

在过去一年里&#xff0c;通用大语言模型&#xff08;LLM&#xff09;的飞速发展引起了全球的关注。百度等科技巨头推出了各自的大模型&#xff0c;不断提高语言模型性能的上限。然而&#xff0c;业界对LLM所设定的目标不再局限于基本的问答功能&#xff0c;而是寻求利用大模型…

mysql入门到精通007-基础篇-事务

1、事务简介 事务是一组操作的集合&#xff0c;它是一个不可分割的事物单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 2、操作演示 开始张三和李四账户表中都是2000元&#xf…

发布订阅模式:观察者模式的一种变体

发布-订阅模型&#xff08;Publish-Subscribe Model&#xff09;的底层机制通常基于观察者模式。 发布-订阅模型是观察者模式的一种变体。 在观察者模式中&#xff0c;主题&#xff08;或被观察者&#xff09;维护了一组观察者&#xff0c;当主题的状态发生变化时&#xff0c…

uni-app 人脸识别 App端

文章目录 背景介绍开发前准备基础版获取视频流人脸识别版本这时候就可以开心的调试了背景介绍 本文介绍如何制作人脸打卡等类似功能的实现。 使用nvue+live-pusher来实现。在App端这是成本较低的可以控制样式的方案了 实现了两个版本 基础版本:视频流 => 抓拍照片 => 传…

信钰证券午评:沪指震荡微涨,券商、银行板块拉升,Sora概念再爆发

23日早盘&#xff0c;沪指盘中强势拉升&#xff0c;一度克复3000点大关&#xff0c;随后震荡回落&#xff1b;深成指、创业板指、科创50指数等均走低&#xff1b;北向资金大幅流出。 截至午间收盘&#xff0c;沪指微涨0.02%报2988.87点&#xff0c;深成指跌0.48%&#xff0c;创…

一、网络基础知识

1、IP地址和端口号 1.1、IP地址 定义&#xff1a;用于在网络中唯一标识设备的地址。格式&#xff1a;通常由四个数字组成&#xff0c;以点分十进制表示&#xff0c;例如&#xff1a;192.168.0.1。(IPv4)作用&#xff1a;允许网络中的设备相互通信&#xff0c;通过IP地址可以定…

navicat导出数据库表结构信息

需求阐述 要求导出某一数据库表中的所有表的结构&#xff0c;汇总成一个word 准备工作 拿到所有表名&#xff0c;在navicat中执行sql语句&#xff1a;show tables;然后点击导出结果&#xff0c;选择excel格式进行导出。 拿到该数据库所有表名后&#xff0c;在navicat中执行如…

vscode【报错】yarn : 无法将“yarn”项识别为 cmdlet

问题 CMD下载完yarn可以查看到yarn版本&#xff0c;但是进入到vscode控制台报错无法识别&#xff0c;报错内容如下&#xff1a; vscode【报错】yarn : 无法将“yarn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff…

Stable Diffusion 模型的概念、类型、下载、安装、使用

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 大家好&#xff0c;我是水滴~~ 我们在《Stable Diffusion WebUI 界面介绍》 时&#xff0c;第一个就讲到了 Stable Diffusion 模型&#xff0c;那么这个模型是什么&#xff1f;该从哪儿下载&…

C++入门学习(三十四)值传递,实参-形参

什么是值传递&#xff1f; 值传递&#xff08;Pass by Value&#xff09;是一种参数传递方式&#xff0c;当函数或方法被调用时&#xff0c;将实际参数的值复制一份传递给函数或方法中的形式参数。这意味着在函数或方法内部对形式参数的修改不会影响到实际参数的值。因为形式参…

关于数组去重new Set的详细解释

用于保持数组的唯一性 // test1 const arr [1, 1, 2, 3, 4, 3] // 是一个new Set对象 const arr1 new Set(arr) console.log(arr1); // test2 const brr [1, 1, 2, 3, 4, 3] // 现在是数组对象了 const brr1 [...new Set(brr)] console.log(brr1); 总结 使用new Set后获…

vscode突然连不上服务器了,以前都可以的,并且ssh等其它方式是可以连接到服务器的

过完年回来准备开工干活&#xff0c;突然发现vscode连不上服务器了&#xff0c;奇了怪了&#xff0c;年前都可以的&#xff0c;看了一下报错&#xff0c;如下&#xff0c; 以为是服务器挂了&#xff0c;结果执行ssh xxxxxx 发现是可以远程连接的&#xff0c;看来服务器没有问题…

3分钟看懂设计模式01:策略模式

一、什么是策略模式 定义一些列算法类&#xff0c;将每一个算法封装起来&#xff0c;并让它们可以互相替换。 策略模式让算法独立于使用它的客户而变化&#xff0c;是一种对象行为型模式。 以上是策略模式的一般定义&#xff0c;属于是课本内容。 在没有真正理解策略模式之…

Unity3d Shader篇(九)— 世界空间法线纹理映射

文章目录 前言一、什么是世界空间法线纹理映射&#xff1f;1. 世界空间法线纹理映射工作原理2. 什么是世界空间&#xff1f;3. 切线空间法线纹理映射和世界空间法线纹理映射对比世界空间法线纹理映射&#xff1a;优点&#xff1a;缺点&#xff1a; 切线空间法线纹理映射&#x…

专145+总420+哈尔滨工业大学803信号与系统和数字逻辑电路考研经验哈工大电子信息与通信,真题,大纲,参考书。

自从高考失利没有考入哈工大&#xff0c;一直带着遗憾&#xff0c;今年初试专业课803信号与系统和数字逻辑电路145&#xff0c;总分420顺利圆满哈工大&#xff0c;了却了一块心病&#xff0c;回看这一年的复习起起落落&#xff0c;心中的那块初心&#xff0c;让我坚持到了上岸&…

springmvc+ssm+springboot房屋中介服务平台的设计与实现 i174z

本论文拟采用计算机技术设计并开发的房屋中介服务平台&#xff0c;主要是为用户提供服务。使得用户可以在系统上查看房屋出租、房屋出售、房屋求购、房屋求租&#xff0c;管理员对信息进行统一管理&#xff0c;与此同时可以筛选出符合的信息&#xff0c;给笔者提供更符合实际的…

外汇天眼:外汇交易不可不知的8大风险!

现在外汇交易中的风险主要有哪些&#xff1f; 外汇作为一种投资方式肯定有风险&#xff0c;我们要想的是尽量规避风险。 今天就给大家介绍一下现在外汇交易中的风险主要有哪些&#xff1f; 一、高杠杆风险 由于外汇保证金交易采用的杠杆比例&#xff0c;放大了损失的额度&…

【前端素材】推荐优质后台管理系统APP Zina平台模板(附源码)

一、需求分析 当我们从多个层次来详细分析后台管理系统时&#xff0c;可以将其功能和定义进一步细分&#xff0c;以便更好地理解其在不同方面的作用和实际运作。 1. 功能层次 a. 用户管理功能&#xff1a; 用户注册和登录&#xff1a;管理用户账户的注册和登录过程。权限管…