用 DDD 把「闹钟」需求一点点捏出来

下面示范一条典型的 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 的层级落地,实现了高内聚、低耦合与可测试性。
相关推荐
Pomelo_刘金3 小时前
Clean Architecture 整洁架构:借一只闹钟讲明白「整洁架构」的来龙去脉
后端·架构·rust
碳酸的唐3 小时前
Inception网络架构:深度学习视觉模型的里程碑
网络·深度学习·架构
五点六六六7 小时前
前端常见的性能指标采集
前端·性能优化·架构
平凡之大路9 小时前
【企业架构】TOGAF概念之二
架构·togaf·企业架构
秋千码途10 小时前
小架构step系列26:Spring提供的validator
java·spring·架构
西陵11 小时前
Nx带来极致的前端开发体验——借助playground开发提效
前端·javascript·架构
Edingbrugh.南空11 小时前
Aerospike架构深度解析:打造web级分布式应用的理想数据库
数据库·架构
人生都在赌12 小时前
从拒绝Copilot到拥抱GPT-5 Agent:一个Team Leader的效能革命
人工智能·架构·devops
沐森12 小时前
今日谈:electron集成appex
架构