下面示范一条典型的 DDD 建模流程,从"说人话"到"写代码"。 时间线 ≈ 2-3 轮白板讨论 + 1-2 轮代码迭代即可完成。
1. 领域访谈 & 统一语言(Ubiquitous Language)
业务说法(原话) | 术语约定 | 说明 |
---|---|---|
"我定一个 7 点的闹钟" | Alarm | 一个提醒装置 |
"响铃" | Ring / Trigger | 当到设定时间并且闹钟启用时发生 |
"提示语" | Message | 通知里展示的内容 |
"关闭闹钟" | Disable | 后续不再响 |
"再次打开闹钟" | Enable | 重新生效 |
2. 领域事件风暴(Event Storming)
- AlarmCreated
- AlarmEnabled / AlarmDisabled
- AlarmTimeChanged / MessageChanged
- AlarmTriggered
- AlarmAcknowledged(用户点了"知道了") 把事件排时间线 → 找出触发者(Command)与读模型。
3. 画上下文图(Bounded Context)
只有一个核心上下文 Alarm,暂不拆子域。 若未来接入"共享闹钟市场""云同步",再分协作上下文。
4. 定 Aggregate + 不变式
Aggregate = Alarm 不变式
- id 唯一
- time 必须 00:00:00--23:59:59
- message 长度 ≤ 140
- 触发条件:enabled && now >= time
- Alarm 作为聚合根,保证以上规则在内存中一次事务内完成。
5. 值对象、实体、枚举
- AlarmId → 值对象(不可变、等值性)
- AlarmTime → 值对象(封装合法性)
- Message → 值对象
- Alarm → 实体(含可变 enabled 状态)
可选
Event: AlarmTriggered(供外部监听)
6. 领域服务(若算法跨越多个对象)
目前只有一个对象,但"计算离闹钟触发还剩多少毫秒"不属于 Alarm 核心属性,可做 Domain Service
rust
pub struct NextTick;
impl NextTick {
pub fn millis_until(alarm: &Alarm, now: NaiveTime) -> u64 { ... }
}
7. Repository 接口(抽象持久化)
rust
pub trait AlarmRepository {
fn save(&self, alarm: Alarm) -> Result<()>;
fn by_id(&self, id: AlarmId) -> Result<Option<Alarm>>;
fn due_since(&self, now: NaiveTime) -> Result<Vec<Alarm>>;
}
放在 调用方所在层(Use-Case)以满足依赖倒置。
8. 把模型落到代码(core/domain)
rust
// value objects
pub struct AlarmId(Uuid);
pub struct AlarmTime(NaiveTime); // 校验合法性
pub struct Message(String);
pub struct Alarm {
id: AlarmId,
time: AlarmTime,
msg: Message,
enabled: bool,
}
impl Alarm {
pub fn trigger_msg(&self) -> &str { &self.msg.0 }
pub fn is_due(&self, now: NaiveTime) -> bool {
self.enabled && now >= self.time.0
}
pub fn disable(&mut self) { self.enabled = false; }
}
9. 用例把模型"编排成剧情"
rust
pub struct CreateAlarm<'a, R: AlarmRepository> { repo: &'a R }
impl<'a, R: AlarmRepository> CreateAlarm<'a, R> {
pub fn exec(&self, t: AlarmTime, msg: Message) -> Result<()> {
let alarm = Alarm { id: AlarmId(Uuid::new_v4()), time: t, msg, enabled: true };
self.repo.save(alarm)
}
}
pub trait Notifier { fn notify(&self, text: &str) -> Result<()>; }
pub struct RingDueAlarms<'a, R: AlarmRepository, N: Notifier> {
repo: &'a R,
notifier: &'a N,
}
impl<'a, R, N> RingDueAlarms<'a, R, N>
where
R: AlarmRepository,
N: Notifier,
{
pub fn exec(&self, now: NaiveTime) -> Result<()> {
for a in self.repo.due_since(now)? {
self.notifier.notify(a.trigger_msg())?;
}
Ok(())
}
}
10. 一张总览图
bash
core/
├─ domain/
│ ├─ alarm.rs ← Entity + VO
│ └─ next_tick.rs ← Domain Service
└─ usecase/
├─ port/
│ ├─ alarm_repo.rs
│ └─ notifier.rs
└─ interactor/
├─ create_alarm.rs
└─ ring_due_alarms.rs
- Adapter / Framework 层去 impl AlarmRepository & Notifier。
- main.rs 负责把 impl 注入 Interactor,启动 CLI/HTTP/任务调度器。
11. 回答"业务接口是谁实现?"
- 业务接口(Domain 内多态,如 PricingStrategy)------由 Domain 或外层共同实现,因其不依赖 IO。
- Port 接口(Repository/Gateway/Notifier)------始终由 Adapter 层 实现。Use-Case 只调用,不实现。 Pomelo_刘金借 DDD 的建模步骤,我们把朴素的"设闹钟"拆成清晰的 实体、不变式、服务、用例、端口,再交给 Clean Architecture 的层级落地,实现了高内聚、低耦合与可测试性。