type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Mar 24, 2024 03:12 AM
Parent item
领域
在前面的章节中,我们介绍了 DDD Lite、CQRS 和 Clean Architecture 等技术。即使单独使用它们是有益的,但它们一起使用效果最好。不幸的是,在实际项目中将它们一起使用并不容易。在本章中,我们将展示如何以最务实、最高效的方式连接 DDD Lite、CQRS 和 Clean Architecture。
我为什么要在乎?
从事编程项目类似于规划和建设住宅区。如果您知道该地区将在不久的将来扩张,您需要为未来的改进保留空间。即使一开始它可能看起来像是浪费空间。您应该为住宅区、医院和寺庙等未来设施留出空间。否则,您将被迫摧毁建筑物和街道,为新建筑物腾出空间。早点考虑一下会好得多。
情况和代码是一样的。如果您知道该项目的开发时间将超过 1 个月,那么您应该从一开始就牢记长远目标。您需要以不会妨碍您未来工作的方式创建代码。即使一开始看起来像是过度设计和大量额外的样板,您也需要牢记长远目标。
这并不意味着您需要规划将来要实现的每一项功能——实际上恰恰相反。这种方法有助于适应新的要求或对我们领域不断变化的理解。这里不需要大的前期设计。这在当今时代至关重要,因为世界变化非常快,谁不能适应这些变化,就可能会被淘汰。
这正是这些模式结合起来所给您带来的——保持恒定开发速度的能力。无需过多破坏和触及现有代码。
是否需要更多的思考和规划?这是一种更具挑战性的方式吗?您需要额外的知识才能做到这一点吗?当然!但从长远来看,这是值得的!幸运的是,您来对地方了。
但让我们把理论抛在脑后吧。让我们看代码。在本章中,我们将跳过设计选择的推理。我们已经在前面的章节中描述了这些。如果您还没有阅读它们,我建议您阅读一下——您会更好地理解本章。与前面的章节一样,我们的代码将基于重构真实的开源项目。这应该使示例更加现实并且适用于您的项目。
你准备好了吗?
让我们开始重构
让我们从 Domain-First 方法开始重构。我们将从领域层的介绍开始。因此,我们将确保实现细节不会影响我们的领域域代码。我们也可以全力以赴地理解业务问题。不是编写无聊的数据库查询和 API 端点。
领域优先方法对于救援(重构)和新建项目都很有效。
为了开始构建我的领域层,我需要确定应用程序实际在做什么。本章将重点关注Wild Workouts 中
Trainings
微服务的重构。我首先确定应用程序处理的case。在之前重构为 Clean Architecture 之后。当我使用混乱的应用层时,我会查看 RPC 和 HTTP 端点以查找支持的case。我确定的功能之一是批准重新安排训练。在Wild Workouts中,如果在日期前 24 小时内提出重新安排训练的请求,则需要审批。如果参加者要求重新安排时间,则需要得到培训师的批准。当培训师提出要求时,学员需要接受。
从领域开始
即使它看起来不像您一生中见过的最糟糕的代码,但像
ApproveTrainingReschedule
这样的函数随着时间的推移往往会变得更加复杂。更复杂的功能意味着未来开发过程中潜在的错误更多。如果我们是这个项目的新手,而且我们没有关于它的“萨满知识”,那么这种情况就更有可能发生。您应该始终考虑在您之后将从事该项目的所有人员,并使其能够防止他们意外破坏项目。这将有助于您的项目不会成为每个人都不敢触碰的遗产。当你刚接触这个项目时,你可能讨厌这种感觉,并且你害怕接触任何东西以免破坏系统。
人们每两年换一次工作的频率并不少见。这使得它对于长期项目开发变得更加重要。
如果您不认为这段代码可能会变得复杂,我建议您检查您所从事的项目中最糟糕的地方的 Git 历史记录。在大多数情况下,最糟糕的代码都是以“几个简单的 if”开始的。代码越复杂,以后简化起来就越困难。我们应该对新出现的复杂性保持敏感,并尽快尝试将其简化。
训练领域实体entity
在分析训练微服务处理的当前用例时,我发现它们都与训练相关。创建一个 Training 类型来处理这些操作是很自然的。
名词==实体 ?
这是发现实体的有效方法吗?嗯,不是真的。
DDD 提供的工具可以帮助我们在无需猜测的情况下对复杂领域进行建模(战略 DDD 模式,聚合)。我们不想猜测我们的聚合是什么样子——我们希望有工具来发现它们。事件风暴技术在这里非常有用……但它是一个单独章节的主题。
这个主题非常复杂,需要用几章来讨论。这就是我们很快要做的事情。
这是否意味着在没有战略 DDD 模式的情况下不应使用这些技术?当然不是!当前的方法对于更简单的项目来说已经足够了。不幸的是(或者幸运的是),并非所有项目都是简单的。
所有字段都是私有的以提供封装。这对于满足 DDD Lite 章节中的“始终在内存中保持有效状态” 规则至关重要。
由于构造函数和封装字段中的验证,我们确信
Training
始终有效。现在,对项目没有任何了解的人无法以错误的方式使用它。同样的规则适用于Training
提供的任何方法。在领域层审批
正如 DDD Lite 简介中所述,我们使用面向行为的方法构建我们的领域。不是靠数据。让我们在领域实体上对
ApproveReschedule
进行建模使用命令编排
现在应用层可以只负责流程的编排。那里没有领域逻辑。我们将整个业务复杂性隐藏在领域层中。这正是我们的目标。为了获取和保存训练,我们使用存储库模式。
取消训练的重构
现在让我们看一下
TrainingService
中的 CancelTraining
。那里的领域逻辑很简单:您可以在训练日期前 24 小时取消训练。如果距培训时间不到 24 小时,但您无论如何都想取消培训:
- 如果您是教练,学员将被取消训练,并额外参加一次课程(没有人喜欢在同一天改变计划!)
- 如果您是学员,您将失去本次训练
当前的实施方式如下:
您可以看到某种用于在取消期间计算训练余额增量的“算法”。这对应用层来说不是一个好兆头。
像这样的逻辑应该存在于我们的领域层中。如果您开始在应用程序层中看到一些与逻辑相关的 if,您应该考虑如何将其移动到领域层。在其他地方测试和重用会更容易。
这可能取决于项目,但通常领域逻辑在初始开发后相当稳定,并且可以长期保持不变。它可以在服务、框架更改、库更改和 API 更改之间移动。
由于这种分离,我们可以以更安全、更快速的方式进行所有这些改变。让我们将
CancelTraining
方法分解为多个独立的部分。这将使我们能够独立测试和更改它们。首先,我们需要处理取消逻辑并将训练标记为已取消。
这里没有什么真正复杂的。那挺好的!
第二个需要移动的部分是计算取消后训练余额的“算法”。理论上,我们可以将其放入
Cancel()
方法中,但在我看来,这会打破单一职责原则 和 CQRS 。而且我喜欢小功能。但该放在哪里呢?一些对象?领域服务?在某些语言中,比如以 J 开头并以 ava 结尾的语言,这是有意义的。但在 Go 中,只需创建一个简单的函数就足够了。
代码现在很简单了。我可以想象我可以和任何非技术人员坐在一起,通过这段代码来解释它是如何工作的。
测试怎么样?这可能有点争议。测试代码将复制该函数的实现。计算算法的任何更改都需要将逻辑复制到测试中。我不会在那里写测试,但如果你想晚上睡得更好——为什么不呢!
将 CancelTraining 移至命令
我们的领域已准备就绪,现在让我们使用它。我们将按照与之前相同的方式进行操作:
- 从存储库获取实体
- 编排领域内容
- 调用外部
trainer
服务取消培训(此服务是“教练日历”的关键点) )
- 返回要保存在数据库中的实体。
存储库重构
存储库的初始实现非常棘手,因为每个用例都有自定义方法。
由于引入了
training.Training
实体,我们可以拥有一个更简单的版本,其中一种方法用于添加新训练,另一种方法用于更新。正如存储库模式中一样,我们使用 Firestore 实现了存储库。我们还将在当前的实现中使用 Firestore。请记住,这是一个实现细节 - 您可以使用任何您想要的数据库。在上一章中,我们展示了使用不同数据库的示例实现。
连接一切
现在如何使用我们的代码?我们的端口层呢?感谢 Miłosz 在《整洁架构》中所做的重构,我们的端口层与其他层解耦。这就是为什么在这次重构之后,它几乎不需要任何重大改变。我们只是调用应用程序命令而不是应用程序服务。
在实际项目中如何进行这样的重构?
如何在实际项目中进行此类重构可能并不明显。进行代码审查并在团队层面就重构方向达成一致是很困难的。
根据我的经验,最好的方法是 Pair或 Mob编程。就算一开始,你可能会觉得浪费时间,知识分享和即时复习会在以后节省很多时间。得益于丰富的知识共享,您可以在初始项目或重构阶段之后更快地工作。
在这种情况下,您不应该考虑 Mob/Pair 编程所损失的时间。你应该考虑一下因为不做而可能损失的时间。它还将帮助您更快地完成重构,因为您无需等待决策。您可以立即就它们达成一致。在实施复杂的新建项目时,群体编程和结对编程也能完美地工作。
在这种情况下,知识共享尤其重要。我多次看到这种方法如何让项目在长期内进展得非常快。当你进行重构时,就合理的时间范围达成一致也很重要。并保留它们。
当您花费整整一个月的时间进行重构时,您可能很快就会失去利益相关者的信任,并且改进并不明显。尽快集成和部署重构也很重要。完美,每天(如果您可以在非重构工作中做到这一点,我相信您也可以在重构中做到这一点!)。如果您的更改长时间未合并和未部署,则会增加破坏功能的机会。它还会阻止重构服务中的任何工作或使更改更难以合并(并不总是能够停止周围的所有其他开发)。
但是什么时候知道项目是否复杂到可以使用 mob 编程呢?不幸的是,没有神奇的公式可以做到这一点。但您应该问自己一些问题:
- 我们了解该领域吗?
- 我们知道如何实施吗?
- 最终会产生一个无人能审查的巨大拉取请求吗?
- 我们是否可以在不进行群体/结对编程的情况下冒更糟糕的知识共享风险?
总结
我们到此结束。重构的完整差异可以在我们的 Wild Workouts GitHub上找到(注意,它很大!)。我希望在本章之后,您还会看到所有引入的模式如何很好地协同工作。如果还没有,请不要担心。我花了三年时间才把所有的点串联起来。但所花的时间是值得的。在我了解了一切是如何相互联系之后,我开始以完全不同的方式看待新项目。从长远来看,它使我和我的团队能够更高效地工作。还值得一提的是,与所有技术一样,这种组合并不是灵丹妙药。如果您正在创建的项目并不复杂,并且在开发 1 个月后不会很快被触及,那么将所有内容放入一个主包中可能就足够了。不过要注意,这 1 个月的开发时间很可能将变成一年!