用 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 的层级落地,实现了高内聚、低耦合与可测试性。
相关推荐
qq_177767371 小时前
React Native鸿蒙跨平台自定义复选框组件,通过样式数组实现选中/未选中状态的样式切换,使用链式调用替代样式数组,实现状态驱动的样式变化
javascript·react native·react.js·架构·ecmascript·harmonyos·媒体
小程故事多_802 小时前
深度搜索Agent架构全解析:从入门到进阶,解锁复杂问题求解密码
人工智能·架构·aigc
●VON3 小时前
React Native for OpenHarmony:项目目录结构与跨平台构建流程详解
javascript·学习·react native·react.js·架构·跨平台·von
Gary董3 小时前
高并发的微服务架构如何设计
微服务·云原生·架构
ujainu3 小时前
Flutter + OpenHarmony 实战:《圆环跳跃》——完整游戏架构与视觉优化
flutter·游戏·架构·openharmony
爬山算法4 小时前
Hibernate(74)如何在CQRS架构中使用Hibernate?
java·架构·hibernate
香芋Yu4 小时前
【大模型教程——第二部分:Transformer架构揭秘】第2章:模型家族谱系:从编码器到解码器 (Model Architectures)
深度学习·架构·transformer
从此不归路6 小时前
Qt5 进阶【13】桌面 Qt 项目架构设计:从 MVC/MVVM 到模块划分
开发语言·c++·qt·架构·mvc
java干货6 小时前
微服务:把一个简单的问题,拆成 100 个网络问题
网络·微服务·架构
橙露8 小时前
Vue3+Pinia实战:从零搭建企业级后台管理系统的核心架构
架构