Event Sourcing与聚合:优雅管理不变性,避免重复检查

event sourcing与聚合:优雅管理不变性,避免重复检查

本文探讨了在事件溯源(Event Sourcing)架构中,聚合(Aggregates)如何高效且不重复地处理业务不变性(invariants)。通过整合相关命令和重新思考“无变化”场景的错误处理,可以优化聚合设计,避免代码冗余,并提升系统的健壮性和可维护性,尤其在处理外部数据更新时。

1. 聚合中不变性检查的挑战

在基于事件溯源的领域驱动设计中,聚合是业务不变性的边界。聚合负责确保其内部状态始终保持有效,这通常通过在其方法中执行不变性检查来实现。然而,当操作涉及多个相关属性,并且这些操作可能由外部源触发时,如何优雅地处理这些不变性检查,避免代码重复和复杂的错误处理逻辑,成为一个常见挑战。

考虑以下一个 ProductAggregateRoot 的示例,其中 changePrice 方法包含了两个不变性检查:

public function changePrice(ChangeProductPrice $command): self{    // 不变性检查1:产品不可用时不能更改价格    if ($this->availability->equals(Availability::UNAVAILABLE())) {        throw CannotChangePriceException::unavailableProduct();    }    // 不变性检查2:如果价格未发生变化,则抛出异常    if ($this->price->equals($command->newPrice)) {        throw CannotChangePriceException::priceHasntChanged();    }    $this->recordThat(        new ProductPriceChanged($this->price, $command->newPrice)    );    return $this;}

当需要从外部数据源同步产品的价格和可用性时,如果采用分别调用 changePrice 和 changeAvailability 方法的方式,可能导致以下问题:

重复的错误处理逻辑: 外部服务需要为每个操作包裹 try-catch 块,例如:

try {    $aggregate->changePrice(new ChangeProductPrice(        $productId,        $state->getPrice()    ));} catch (CannotChangePriceException $ex) {    // 处理价格变更失败}try {    $aggregate->changeAvailability(new ChangeProductAvailability(        $productId,        $state->getAvailability()    ));} catch (CannotChangeAvailabilityException $ex) {    // 处理可用性变更失败}

这种方式不仅冗长,而且难以处理多个操作之间的上下文关联。

不变性检查的重复: 如果为了在调用聚合方法前进行预检查,而在外部服务中也实现 canChangePrice() 这样的方法,将导致不变性逻辑在聚合内部和外部的双重存在,增加了维护成本和出错风险。

2. 整合命令以实现上下文感知的检查

解决上述问题的关键在于重新思考命令的粒度。当多个属性的变更在业务上是紧密关联的,并且它们的有效性检查需要相互协作时,应该将这些操作封装到一个更高级别的命令中。

例如,与其分别处理价格和可用性,不如创建一个 UpdateProductDetails 或 ChangeProductPriceAndAvailability 这样的命令。这个命令将包含所有相关信息,并传递给聚合的一个新方法。

新的命令示例:

final class UpdateProductDetails{    public function __construct(        private ProductId $productId,        private Money $newPrice,        private Availability $newAvailability    ) {}    public function getProductId(): ProductId { return $this->productId; }    public function getNewPrice(): Money { return $this->newPrice; }    public function getNewAvailability(): Availability { return $this->newAvailability; }}

聚合中处理整合命令的方法:

class ProductAggregateRoot // ...{    public function updateDetails(UpdateProductDetails $command): self    {        // 假设我们允许在产品不可用时更新其可用性,但价格更新仍受可用性限制。        // 通过整合命令,聚合可以获得更全面的上下文。        // 检查价格变更的不变性:        // 如果产品当前不可用,且新的可用性也不是“可用”,则不允许价格变更。        // 或者,如果新的可用性是“可用”,则可以忽略当前可用性状态对价格变更的限制。        if ($this->availability->equals(Availability::UNAVAILABLE()) &&             !$command->getNewAvailability()->equals(Availability::AVAILABLE())) {            // 如果产品当前不可用,且更新后仍不可用,则不能更改价格            if (!$this->price->equals($command->getNewPrice())) {                throw CannotChangePriceException::unavailableProduct();            }        }        // 处理价格变更        if (!$this->price->equals($command->getNewPrice())) {            $this->recordThat(                new ProductPriceChanged($this->price, $command->getNewPrice())            );        }        // 处理可用性变更        if (!$this->availability->equals($command->getNewAvailability())) {            $this->recordThat(                new ProductAvailabilityChanged($this->availability, $command->getNewAvailability())            );        }        return $this;    }}

通过这种方式,聚合在 updateDetails 方法中可以一次性访问所有相关的输入,从而执行更具上下文感知的、更强大的不变性检查。外部服务只需要发送一个命令,聚合内部负责所有复杂的业务逻辑和不变性验证。

3. 重新思考“无变化”的错误处理

原始 changePrice 方法中的 priceHasntChanged 异常值得商榷。如果一个命令表达的是“我希望价格成为 X”,而当前价格已经是 X,那么这通常不应该被视为一个错误,而是一个“无操作”(no-op)行为。

将“无变化”视为错误会迫使调用者在发送命令前先查询聚合的当前状态,这违背了命令的意图——命令应该表达意图,而不是要求先知。

优化后的聚合方法示例:

public function changePrice(ChangeProductPrice $command): self{    // 不变性检查:产品不可用时不能更改价格    if ($this->availability->equals(Availability::UNAVAILABLE())) {        throw CannotChangePriceException::unavailableProduct();    }    // 如果价格未发生变化,则不记录事件,直接返回聚合实例    if ($this->price->equals($command->newPrice)) {        return $this; // 视为无操作,不抛出异常    }    $this->recordThat(        new ProductPriceChanged($this->price, $command->newPrice)    );    return $this;}

在 updateDetails 方法中,同样可以应用此原则:

public function updateDetails(UpdateProductDetails $command): self{    // ... (不变性检查逻辑,例如对价格的可用性限制) ...    $events = [];    // 处理价格变更    if (!$this->price->equals($command->getNewPrice())) {        $events[] = new ProductPriceChanged($this->price, $command->getNewPrice());    }    // 处理可用性变更    if (!$this->availability->equals($command->getNewAvailability())) {        $events[] = new ProductAvailabilityChanged($this->availability, $command->getNewAvailability());    }    // 如果有任何事件需要记录,则记录它们    if (!empty($events)) {        foreach ($events as $event) {            $this->recordThat($event);        }    }    return $this;}

通过这种方式,如果所有期望的变更都与当前状态一致,聚合将不会记录任何事件,并且不会抛出异常。这使得客户端代码更简洁,不需要预先检查状态,也更符合命令式编程的风格。

4. 最佳实践与注意事项

命令粒度与业务意图: 命令的粒度应与业务意图相匹配。当一系列操作在业务上构成一个不可分割的单元时,应将其封装为一个命令。这有助于聚合在执行不变性检查时拥有足够的上下文信息。聚合职责的单一性: 聚合应专注于其内部不变性的维护。所有影响聚合状态的决策和验证都应在其内部完成。外部服务或应用层应只负责发送命令,而不应重复聚合的业务逻辑。事件的粒度: 尽管命令可以被整合,但生成的事件应保持其原子性。例如,一个 UpdateProductDetails 命令可能导致 ProductPriceChanged 和 ProductAvailabilityChanged 两个独立的事件被记录。事件应该反映“发生了什么”,而不是“我们想做什么”。幂等性: 优雅地处理“无变化”情况有助于实现命令的幂等性。无论命令被执行多少次,只要聚合最终达到期望的状态,就不会产生额外的副作用(即重复的事件)。领域服务与聚合: 如果不变性检查跨越多个聚合,则可能需要领域服务来协调这些聚合。但对于单个聚合内部的不变性,始终应由聚合本身负责。错误处理: 仅当业务规则被真正违反时才抛出异常。对于那些仅仅表示“状态已满足期望”的情况,应返回聚合实例,而不记录事件。

总结

在事件溯源和聚合设计中,有效管理不变性是构建健壮领域模型的关键。通过以下策略,我们可以避免不变性检查的重复,简化客户端代码,并提升系统的可维护性:

整合相关命令: 将业务上紧密关联的操作封装到单个命令中,赋予聚合更全面的上下文来执行复杂的、多维度的不变性检查。优化“无变化”处理: 将目标状态已达成的场景视为“无操作”而非错误,避免不必要的异常抛出,并简化客户端的调用逻辑。

遵循这些原则,可以构建出更清晰、更健壮、更易于理解和扩展的聚合,从而更好地支持复杂的业务逻辑。

以上就是Event Sourcing与聚合:优雅管理不变性,避免重复检查的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1321284.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
在HTML/PHP中正确调用外部JavaScript文件中的函数
上一篇 2026年5月10日 11:00:32
如何在Python中设置Cookie?
下一篇 2026年5月10日 11:00:42

相关推荐

  • 九天算力平台任务:本地电脑关机后,计算任务还会继续运行吗?

    九天算力平台:本地电脑关闭后任务运行状态详解 使用九天算力平台进行AI训练时,许多用户关心一个问题:本地电脑关机后,平台上的计算任务能否继续运行? 部分用户反馈,关闭VS Code后,任务似乎停止,需要重新启动,这与预期中的远程服务器持续运行不符。 虽然平台后台显示计算时间仍在继续(用户截图所示),…

    2026年5月10日
    200
  • html如何建立副标题_为HTML文档添加副标题标签【标签】

    推荐使用与标签组合:主标题用,副标题用带class=”subtitle”的,语义清晰且不破坏大纲;已废弃但部分浏览器支持;ARIA可增强可访问性;CSS伪元素适合固定文本场景。 如果您希望在HTML文档中为标题添加副标题,以提供更详细的说明或补充信息,则需要使用语义化的方式组…

    2026年5月10日
    000
  • C++标记模式 运行时类型识别替代

    标记模式是一种基于类型标签在编译期实现函数分发的技术,通过定义标签类型(如tag_derived_a)并结合虚函数返回对应标签,利用if constexpr在编译期判断类型并调用相应逻辑,避免了RTTI开销,适用于嵌入式或性能敏感场景,但需手动扩展标签且灵活性低于dynamic_cast。 在C++…

    2026年5月10日
    000
  • Go语言随机数生成详解:如何获得每次运行都不同的随机数

    在Go语言中,rand 包提供了生成伪随机数的功能。然而,初学者经常遇到的一个问题是,每次运行程序时,生成的随机数序列都是相同的。这是因为 rand 包使用固定的默认种子来初始化随机数生成器。为了获得每次运行都不同的随机数,我们需要手动设置种子。 使用当前时间作为种子 最常用的方法是使用当前时间作为…

    2026年5月10日
    100
  • React + AWS Cognito:电子邮件身份验证设置指南(第二部分)

    在上一篇文章中,我们处理了 aws 端的所有内容;现在让我们深入研究 react 来设置我们的代码。 aws 提供了 npm 包 @aws-sdk/client-cognito-identity-provider,其中包含以下功能: 使用电子邮件和密码创建帐户通过 aws 发送的代码验证电子邮件使用…

    用户投稿 2026年5月10日
    000
  • PHP 并发文件操作中的数据完整性保障:使用文件锁防止数据丢失

    本文旨在解决服务器端在处理高并发文件写入时可能发生的数据丢失问题。当多个请求同时尝试修改同一文件时,可能导致竞态条件。通过引入 PHP 的文件锁(`flock`)机制,可以确保文件在写入过程中被独占访问,从而有效防止数据损坏或丢失,保障数据传输和存储的原子性与一致性。 在现代 Web 应用中,客户端…

    2026年5月10日
    000
  • 如何在Python中设置Cookie?

    在python中,可以使用http.cookies模块或flask框架来设置cookie。使用flask设置cookie的步骤如下:1.创建响应对象,2.使用set_cookie方法设置cookie的名称、值和有效期。设置cookie时需考虑key、value、max_age、expires、pat…

    2026年5月10日
    000
  • Go语言中实现多态对象工厂模式的最佳实践

    本文探讨了在go语言中如何设计一个能够根据输入创建不同类型对象的工厂函数。针对初学者常遇到的直接返回具体类型或空接口导致编译失败的问题,文章详细阐述了通过定义并返回接口类型来解决这一挑战。这种方法利用go语言的隐式接口实现特性,有效构建出灵活且可扩展的对象工厂,从而实现多态行为。 Go语言对象工厂模…

    2026年5月10日
    000
  • 什么是XPath?如何定位XML节点?

    XPath是一种在XML/HTML文档中精准定位节点的语言,通过路径表达式、属性、文本内容及轴(如父、兄弟节点)实现灵活查找。它优于CSS选择器之处在于支持向上遍历、基于文本定位和复杂逻辑判断,适用于自动化测试、爬虫等场景,但需避免脆弱性、性能问题和可读性差等陷阱。编写健壮的XPath应优先使用唯一…

    2026年5月10日
    000
  • Robocorp Browser库截图超时错误解析与稳健重试策略

    Robocorp自动化过程中,使用Browser库的take_screenshot功能时,常因内部“聚焦”机制不稳定而遭遇超时错误。本文深入解析该问题,并提出一种高效且稳健的重试策略作为核心解决方案,通过代码示例详细阐述如何实现多次尝试截图,显著提升自动化脚本的可靠性,确保关键截图操作的成功执行,避…

    2026年5月10日
    000
  • Go语言中ISO-8859-1到UTF-8的转换机制解析

    本文深入解析go语言中将iso-8859-1编码文本转换为utf-8的机制。核心在于iso-8859-1字符与unicode前256个码点的一致性,使得每个iso-8859-1字节可直接转换为对应的unicode `rune`。随后,`bytes.buffer`的`writerune`方法负责将这些…

    2026年5月10日
    000
  • 格式化和 Linting 以保持一致性

    此活动涉及在我的开源项目 genereadme 中实施统计分析工具,以提高代码质量和一致性。 克莱布恩特拉 / 基因自述文件 genereadme 是一个命令行工具,它接收源代码文件并生成 readme.md 文件,该文件利用 llm 解释文件中的代码。 贡献 欢迎为 genereadme 做出贡献…

    2026年5月10日
    000
  • LangChain表达式语言:多链间变量传递与状态管理

    本文深入探讨了LangChain表达式语言中跨链变量传递与状态管理的挑战与解决方案。当构建复杂的LLM应用时,常需将原始输入变量与前一链的输出结果一同传递给后续链。文章通过具体代码示例,详细阐述了如何利用operator.itemgetter高效、明确地实现这一目标,确保原始上下文信息在多链流程中得…

    2026年5月10日
    000
  • Go语言defer语句:资源管理与异常处理的利器

    本文深入探讨Go语言中的defer语句,它是实现资源安全释放和优雅异常处理的关键机制。defer语句确保函数调用在外部函数返回前执行,常用于资源清理如解锁或关闭文件。文章将详细阐述defer的LIFO(后进先出)执行顺序,并通过具体代码示例展示其在资源管理中的应用,以及如何与panic和recove…

    2026年5月10日
    000
  • 返回变长序列:Go 语言的惯用方法

    在 Go 语言中,函数返回变长序列是一个常见的需求。本教程将以生成斐波那契数列为例,介绍如何以惯用的方式实现这一功能,并讨论序列长度已知和未知两种情况下的不同处理方式。 序列长度已知的情况 如果事先知道序列的长度,最佳实践是使用 make 函数预先分配切片。这样做可以避免在循环中频繁地重新分配内存,…

    2026年5月10日
    000
  • php怎么用input_PHP表单input数据获取与处理方法

    使用$_POST、$_GET、filter_input等方法可安全获取表单数据,结合验证与过滤确保输入有效且防攻击。 如果您在使用PHP开发网页表单功能时,需要获取用户通过input输入的数据,可以通过预定义的超全局变量来实现数据的接收与处理。以下是几种常见的获取和处理表单input数据的方法: 一…

    2026年5月10日
    000
  • Go语言中实现操作系统特定逻辑的最佳实践

    go语言通过文件命名约定(pkgname_osname.go)提供了一种优雅的机制,用于在编译时根据目标操作系统选择性地包含代码。这使得开发者能够在单个项目树中编写平台特定的功能,如处理系统启动项,有效避免了传统条件编译的复杂性,确保了代码的整洁与高效。 在开发跨平台应用程序时,我们经常会遇到需要与…

    2026年5月10日
    000
  • Go语言中http.Get方法为何会造成内存泄漏?

    Go语言http.Get方法潜在的内存泄漏 本文分析了使用Go语言net/http包中的http.Get方法时可能出现的内存泄漏问题。 问题描述 以下Go代码片段演示了该问题: 立即学习“go语言免费学习笔记(深入)”; func main() { go gettest() select {}}fu…

    2026年5月10日
    000
  • HTML行高间距怎么设置_文本可访问性排版指南

    行高应设为字体大小的1.5至2倍以提升可读性,推荐使用无单位数值(如line-height: 1.5)以实现响应式自适应,避免固定像素值导致的可访问性问题。该设置结合合适的字体大小、对比度、字间距和文本对齐方式,能显著改善各类用户的阅读体验,尤其利于阅读障碍者。在响应式设计中,配合rem、em等相对…

    2026年5月10日
    000
  • JavaScript动态生成与更新JSON-LD Schema脚本教程

    本教程详细阐述了如何利用JavaScript动态生成并更新网页中的JSON-LD结构化数据脚本。通过构建数据对象、创建脚本元素、序列化JSON数据并将其附加到文档头部,您可以实现对产品评分、价格等动态内容的实时更新,从而提升搜索引擎对网页内容的理解和展示效果。 1. 理解JSON-LD与动态数据需求…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信