Table of contents
Open Table of contents
DDD 解决什么问题
没有 DDD 的项目:
代码按技术分层: controllers/ models/ services/ utils/
业务逻辑散落在 controller 里、service 里、甚至 SQL 里
新人来了看不懂业务,改一个功能要改 5 个文件
"这个扣血逻辑在 PlayerService 里还是 CombatService 里?两边都有一点"
DDD 的核心主张:
按业务领域组织代码,而不是按技术层次
让代码结构反映业务结构
业务逻辑集中在领域层,不泄漏到基础设施
核心概念
1. 统一语言(Ubiquitous Language)
开发团队和业务方必须用同一套术语。代码里的命名 = 业务文档里的命名。
业务方说: "玩家从背包中取出物品放到市场上架"
代码应该:
player.inventory.remove_item(item_id)
marketplace.list_item(item, price)
而不是:
db.update("items", {container_id: null})
db.insert("listings", {item_id, price})
术语统一的好处:和策划讨论需求时,代码里的 Player、Inventory、Marketplace 他们都认识。
2. 实体(Entity)
有唯一标识的对象。即使所有属性都变了,只要 ID 相同就是同一个实体。
struct Player {
id: PlayerId, // 唯一标识
name: String, // 可以改
level: u32, // 可以变
health: f32, // 每帧都在变
// 不管怎么变,id 相同就是同一个玩家
}
// ID 用新类型包装,防止和其他 ID 混淆
struct PlayerId(Uuid);
struct ItemId(Uuid);
// PlayerId 和 ItemId 不能互相传递——编译器阻止你犯错
3. 值对象(Value Object)
没有唯一标识,靠值本身来区分。相等 = 所有字段相等。不可变。
#[derive(Clone, PartialEq)]
struct Money {
amount: u64, // 分为单位,避免浮点精度问题
currency: Currency,
}
#[derive(Clone, PartialEq)]
struct Position {
x: f32,
y: f32,
z: f32,
}
// Money(100, RUB) == Money(100, RUB) → true(值相同)
// 没有 id 字段,100 卢布就是 100 卢布
实体 vs 值对象:
- 两个玩家名字相同 → 不是同一个玩家(靠 ID 区分)→ 实体
- 两笔 100 卢布 → 没区别(靠值区分)→ 值对象
4. 聚合(Aggregate)
一组相关对象的边界。外部只能通过聚合根(Aggregate Root)访问内部对象。
// Player 是聚合根
// Inventory 和 Item 是聚合内部对象
struct Player {
id: PlayerId,
name: String,
level: u32,
inventory: Inventory, // 内部对象,外部不能直接操作
}
struct Inventory {
items: Vec<Item>,
max_slots: u32,
}
impl Player {
// 外部通过 Player(聚合根)操作 Inventory
pub fn add_item(&mut self, item: Item) -> Result<(), DomainError> {
if self.inventory.is_full() {
return Err(DomainError::InventoryFull);
}
if self.level < item.required_level {
return Err(DomainError::LevelTooLow {
required: item.required_level,
current: self.level,
});
}
self.inventory.items.push(item);
Ok(())
}
pub fn remove_item(&mut self, item_id: ItemId) -> Result<Item, DomainError> {
let pos = self.inventory.items.iter()
.position(|i| i.id == item_id)
.ok_or(DomainError::ItemNotFound(item_id))?;
Ok(self.inventory.items.remove(pos))
}
}
// 错误做法:绕过聚合根直接操作内部
// player.inventory.items.push(item) ← 跳过了容量检查和等级检查
为什么要聚合? 保证业务不变量(invariant)。“背包不能超过上限”、“等级不够不能装备”这些规则写在聚合根的方法里,外部绕不过去。
5. 领域事件(Domain Event)
领域中发生的有意义的事情。解耦模块间的依赖。
enum DomainEvent {
PlayerLeveledUp { player_id: PlayerId, new_level: u32 },
ItemPickedUp { player_id: PlayerId, item: Item },
TradeCompleted { buyer_id: PlayerId, seller_id: PlayerId, item: Item, price: Money },
PlayerKilled { victim_id: PlayerId, killer_id: PlayerId, weapon: String },
}
impl Player {
pub fn gain_exp(&mut self, amount: u64) -> Vec<DomainEvent> {
let mut events = vec![];
self.exp += amount;
while self.exp >= self.exp_to_next_level() {
self.exp -= self.exp_to_next_level();
self.level += 1;
events.push(DomainEvent::PlayerLeveledUp {
player_id: self.id.clone(),
new_level: self.level,
});
}
events
}
}
// 其他模块监听事件做自己的事,不需要知道升级逻辑
// PlayerLeveledUp → 成就系统检查是否解锁成就
// PlayerLeveledUp → 排行榜更新
// PlayerLeveledUp → 推送通知给好友
// 升级逻辑不需要知道这些模块的存在
6. 仓储(Repository)
对持久化的抽象。领域层定义接口,基础设施层提供实现。
// 领域层:只定义 trait(不知道数据库是什么)
#[async_trait]
trait PlayerRepository {
async fn find_by_id(&self, id: &PlayerId) -> Result<Option<Player>, DomainError>;
async fn save(&self, player: &Player) -> Result<(), DomainError>;
async fn find_by_name(&self, name: &str) -> Result<Option<Player>, DomainError>;
}
// 基础设施层:具体实现(知道用的是 PostgreSQL)
struct PgPlayerRepository {
pool: PgPool,
}
#[async_trait]
impl PlayerRepository for PgPlayerRepository {
async fn find_by_id(&self, id: &PlayerId) -> Result<Option<Player>, DomainError> {
sqlx::query_as("SELECT * FROM players WHERE id = $1")
.bind(id.0)
.fetch_optional(&self.pool)
.await
.map_err(DomainError::from)
}
// ...
}
// 测试时可以换成内存实现
struct InMemoryPlayerRepository {
players: HashMap<PlayerId, Player>,
}
7. 领域服务(Domain Service)
不属于任何单个实体的业务逻辑。通常涉及多个聚合的协调。
struct TradeService;
impl TradeService {
pub fn execute_trade(
buyer: &mut Player,
seller: &mut Player,
item_id: ItemId,
price: Money,
) -> Result<Vec<DomainEvent>, DomainError> {
// 业务规则全在这里
if buyer.wallet.balance < price {
return Err(DomainError::InsufficientFunds);
}
let item = seller.remove_item(item_id)?;
buyer.add_item(item.clone())?;
buyer.wallet.deduct(price.clone())?;
seller.wallet.add(price.clone())?;
Ok(vec![DomainEvent::TradeCompleted {
buyer_id: buyer.id.clone(),
seller_id: seller.id.clone(),
item,
price,
}])
}
}
限界上下文(Bounded Context)
大系统里不同模块对同一个词有不同理解:
"玩家" 在不同上下文中的含义:
战斗上下文: Player = { id, position, health, weapon, velocity }
社交上下文: Player = { id, name, friends, online_status }
交易上下文: Player = { id, wallet, inventory, trade_history }
匹配上下文: Player = { id, skill_rating, queue_time, region }
强行用一个 Player 结构体塞所有字段 → 变成上帝对象。
每个限界上下文有自己的 Player 定义,上下文之间通过 ID 关联,通过事件或 API 通信。
mini_tarkov 的限界上下文:
┌── 战斗上下文 ──┐ ┌── 交易上下文 ──┐ ┌── 社交上下文 ──┐
│ Player(战斗) │ │ Player(交易) │ │ Player(社交) │
│ Weapon │ │ Inventory │ │ FriendList │
│ DamageCalc │ │ Marketplace │ │ ChatMessage │
│ HitDetection │ │ TradeService │ │ Notification │
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
└───── 通过 PlayerId 关联 + 领域事件通信 ──┘
DDD 在 Rust 项目中的目录结构
src/
├── domain/ # 领域层:纯业务逻辑,无外部依赖
│ ├── mod.rs
│ ├── player.rs # Player 聚合根 + Inventory
│ ├── item.rs # Item 实体
│ ├── trade.rs # TradeService 领域服务
│ ├── events.rs # DomainEvent 枚举
│ ├── errors.rs # DomainError
│ └── repository.rs # Repository trait 定义(只有接口)
│
├── application/ # 应用层:编排用例,调用领域层
│ ├── mod.rs
│ ├── commands.rs # CreatePlayer, AddItem, ExecuteTrade...
│ └── handlers.rs # 用例处理:协调 repository + domain service
│
├── infrastructure/ # 基础设施层:具体实现
│ ├── mod.rs
│ ├── postgres.rs # PgPlayerRepository 实现 Repository trait
│ ├── redis.rs # Redis 缓存实现
│ └── config.rs # 配置加载
│
├── api/ # 接口层:HTTP/gRPC handler
│ ├── mod.rs
│ ├── http.rs # axum handler
│ ├── grpc.rs # tonic handler
│ └── dto.rs # 请求/响应的数据传输对象
│
├── lib.rs
└── main.rs
DDD 不是银弹
适合 DDD 的:
业务逻辑复杂(交易系统、匹配系统、战斗规则)
业务规则经常变化
多团队协作,需要清晰的边界
不适合 DDD 的:
简单 CRUD(博客、TODO 应用)— 过度设计
数据密集型管道(ETL、日志处理)— 没什么领域逻辑
原型/MVP — 先跑起来再说
DDD 的代价:
更多的抽象层 → 代码量增加
Repository trait → 需要维护接口 + 实现
值对象 → 需要大量的 From/Into 转换
前期设计成本高(需要理解业务领域)