Rust项目中的领域驱动设计:当类型成为你的盟友
你是否曾经编写过Rust应用程序,其业务逻辑淹没在结构体、trait和宏的海洋中?当你看着代码,却无法立即说出某个函数在领域上下文中是做什么的,这种感觉是否似曾相识?如果是这样——欢迎加入俱乐部。领域驱动设计(DDD)旨在解决这个问题,而Rust及其类型系统是实施DDD原则的理想平台。
在本文中,我们不会复述埃文斯的书。相反,我将通过实际示例展示如何在Rust项目中应用领域驱动设计。你将了解如何利用包装类型、模块化架构和富有表现力的结构体,使领域代码变得清晰、安全且易于理解。我们开始吧!
为什么Rust和DDD是理想的一对
领域驱动设计要求将领域逻辑与基础设施严格隔离。Rust为此提供了强大的工具:
- 类型系统——允许在编译器级别对业务规则进行建模。错误?编译器不会让你犯这个错。
- 所有权和借用——确保聚合和实体不会在意想不到的地方被意外修改。
- 模块系统——清晰地将领域层、基础设施和应用程序分开。
但也有一些陷阱。Rust不会容忍带有大量可变状态的“臃肿”对象。因此,Rust中的DDD需要与Java或C#等不同的建模方法。
关键思想:使无效状态不可表示
在DDD中,我们力求将业务规则编码到数据结构本身中。在Rust中,这变成了一句口头禅:“如果状态无效——那就让它无法被创建”。
与其这样写:
struct User { name: String, email: String, age: u8,}我们创建在编译阶段就能保证正确性的类型:
pub struct Email(String);pub struct Age(u8);pub struct UserName(String);
pub struct User { name: UserName, email: Email, age: Age,}看起来这只是一些包装器?实际上,这是迈向Rust中的DDD的第一步,其中类型既是文档,也是不变量的守护者。
建模领域:值对象和实体
DDD的核心概念是Entity(实体)和Value Object(值对象)。它们在Rust中的实现方式不同。
Rust中的值对象:不变性和按值相等
值对象没有标识。如果两个对象的字段相等,则它们相等。在Rust中,这完美地适用于带有#[derive(PartialEq)]和不可变字段的结构体。
use std::fmt;
#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}
#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}
impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // 业务规则:金额不能为负 assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }
pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}请注意:Money是不可变的。任何操作都返回一个新对象。这符合Rust中的DDD哲学:没有setter,只有创建新实例的方法。