Clean Architecture 整洁架构:借一只闹钟讲明白「整洁架构」的来龙去脉

1 进化史与里程碑

年份 思想 / 架构 主要诉求 引用人物
1978 MVC(Smalltalk-80) 将 UI-状态-动作解耦 Trygve Reenskaug
1990s 3-Tier / N-Layer(Web 盛行) Presentation / Business / Data 隔离 企业应用模式
2005 Hexagonal Architecture(Ports & Adapters) UI、DB 都只是"外设";靠 Port 接口 隔离 Alistair Cockburn
2008 Onion Architecture 把 Hexagon 画成同心圆:依赖只能向内 Jeffrey Palermo
2012 Clean Architecture 汇总 Onion + Hexagon + DDD,用更明确的四层语义推广 Robert C. Martin ("Uncle Bob")
2017 《Clean Architecture》一书 理论体系化、案例丰富 Uncle Bob

这些演进围绕同一个痛点:业务代码的寿命远长于任何技术细节

只要让"框架/数据库/协议"留在外圈,核心业务就能:

  • 不被技术更新拖着走。
  • 可以在内存里飞快地做单元测试。
  • 被不同界面(Web、CLI、Scheduler...)复用。

2 Clean Architecture 的四层剖面

  1. Entities(Enterprise Business Rules)
  • 领域对象、值对象、纯算法
  1. Use Cases(Application Business Rules)
  • 一次业务场景的「剧本」
  • 定义 Port trait 任意调用外部资源
  1. Interface Adapters
  • Controller / Presenter
  • Repository / Gateway 实现这些 Port
  • DTO ↔ Entity 的 Mapper
  1. Frameworks & Drivers
  • Web 框架、ORM、消息队列、第三方 SDK
  • Composition Root:启动 + 依赖注入

依赖只能内向;内层永远不知道外层存在


3 为什么要用 Clean Architecture

  1. 可测试 内层代码零 IO;Mock Port 就能跑毫秒级单测。
  2. 可演化 Web→gRPC、MySQL→Mongo 只改最外圈。
  3. 可复用 同一套 Use-Case 能同时服务 App、CLI、Batch Job。
  4. 可维护 业务规则与技术细节分仓,阅读、重构心智负担小。

4 一个典型的目录布局(语言无关)

bash 复制代码
src/
├─ core/                    ← 不依赖任何框架
│   ├─ domain/              ← Entities + Domain Services
│   │     ├─ entities/
│   │     └─ services/
│   └─ usecase/             ← Interactors + Port traits
│         ├─ interactor/
│         └─ port/
├─ adapters/                ← 实现 Port,做格式转换
│   ├─ persistence/
│   ├─ notification/
│   └─ web/
├─ frameworks/              ← 具体框架 glue code
│   └─ actix/, diesel/, ...
└─ main.rs / App.kt / Program.cs ← Composition Root

核心判别法:

core/ 不准 import 任何外部库(除了官方标准库)。 其它层可以反向依赖 core。


5 闹钟需求:一步步落到代码

下面举一个例子:

用户可以创建若干闹钟,指定时间与提示文本;到点后系统提醒。

5.1 领域建模(core/domain)

领域建模使用 DDD(# Domain-driven-design)进行建模.该文章只介绍整洁架构,对于 DDD的讲解请移步:Domain-driven-design

rust 复制代码
// entities/alarm.rs
pub struct AlarmId(Uuid);

pub struct AlarmTime(NaiveTime);           // HH:mm:ss,省掉日期

pub struct Alarm {
    id: AlarmId,
    time: AlarmTime,
    message: String,
    enabled: bool,
}

impl Alarm {
    pub fn trigger_msg(&self) -> String { self.message.clone() }
    pub fn is_due(&self, now: NaiveTime) -> bool { self.enabled && now >= self.time }
}

// services/next_tick.rs
pub struct NextTick;
impl NextTick {
    pub fn millis_until(alarm: &Alarm, now: NaiveTime) -> u64 { ... }
}

纯净:只依赖 chrono 之类标准日期库,无 IO。

5.2 Use Case + Port(core/usecase)

rust 复制代码
// port/alarm_repo.rs  ------ 访问持久层
pub trait AlarmRepository {
    fn save(&self, alarm: Alarm) -> anyhow::Result<()>;
    fn find_due(&self, now: NaiveTime) -> anyhow::Result<Vec<Alarm>>;
}

// port/notifier.rs  ------ 发送提醒
pub trait Notifier {
    fn notify(&self, msg: &str) -> anyhow::Result<()>;
}

// interactor/create_alarm.rs
pub struct CreateAlarm<'a, R: AlarmRepository> { repo: &'a R }
impl<'a, R: AlarmRepository> CreateAlarm<'a, R> {
    pub fn exec(&self, time: AlarmTime, msg: String) -> anyhow::Result<()> {
        let alarm = Alarm { id: AlarmId::new(), time, message: msg, enabled: true };
        self.repo.save(alarm)
    }
}

// interactor/ring_due_alarms.rs
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) -> anyhow::Result<()> {
        for alarm in self.repo.find_due(now)? {
            self.notifier.notify(&alarm.trigger_msg())?;
        }
        Ok(())
    }
}

要点:

这里的 trait (AlarmRepository, Notifier) 就是 Port 即IO,但是IO的具体实现不在这里; 用例不知道数据库、HTTP、推送 SDK 长啥样。

5.3 Adapter 层(adapters/...)

rust 复制代码
// adapters/persistence/sqlite_repo.rs
pub struct SqliteRepo { conn: Connection }
impl AlarmRepository for SqliteRepo { /* SQL 读写 */ }

// adapters/notification/desktop_notifier.rs
pub struct DesktopNotifier;
impl Notifier for DesktopNotifier {
    fn notify(&self, msg: &str) { /* 调 OS API 弹窗 */ }
}

外部IO的具体实现

5.4 框架 & 组合根(main.rs

rust 复制代码
fn main() -> anyhow::Result<()> {
    // === 组装 ===
    let repo = SqliteRepo::new("alarms.db")?;
    let notifier = DesktopNotifier::new();
    let create_uc = CreateAlarm { repo: &repo };
    let ring_uc   = RingDueAlarms { repo: &repo, notifier: &notifier };

    // === CLI 入口示例 ===
    if let Some(("add", cmd)) = cli().subcommand() {
        create_uc.exec(parse_time(cmd), cmd.value_of("msg").unwrap().into())?;
    } else {                          // 简易轮询定时器
        loop {
            ring_uc.exec(Local::now().time())?;
            thread::sleep(Duration::from_secs(30));
        }
    }
}

main 里把 Port 绑到 Adapter,实现依赖注入;核心业务仍然 0 framework import。


6 常见疑问:那些接口究竟谁来实现?

  1. Port trait(访问 IO)的实现在 Adapter / Framework 层,_绝不能_回到 Use Case。
  2. 业务多态 trait(如 PricingStrategy)如果出现在 Domain,可以在 Domain 里给出若干实现,也可以让 Use Case / Adapter 扩展------只要它不需要外部资源即可。
  3. "用例自己要不要实现接口?"
  • 用例实现 输入端口(例如 MVC Controller 调用 execute())的场景可以有;
  • 但它绝不实现 输出端口(Repository、Gateway),否则又把 IO 拉回内层。

口诀:

"接口属于调用方,具体类属于被调用方的外层。"


7 总结

  • MVC / N-Layer 解决了 UI 与 DB 的早期耦合,但没挡住"框架侵入"。
  • Hexagonal 用 Port 把 UI、DB 看成"六边形的插口";Onion 再次强调依赖朝内。
  • Clean Architecture 把名字与职责说得更白:Entities、Use Cases、Adapters、Frameworks。

目录、trait、struct 的划分只为一件事:

让你今天写下的业务规则,十年后还能在任何新技术栈上跑起来。

Pomelo_刘金 拿闹钟示例演练一遍,你就可以把相同套路复制到电商、IM、金融交易或任何需要长期维护的系统里。祝编码整洁,业务长青。

相关推荐
白-胖-子3 小时前
深入剖析大模型在文本生成式 AI 产品架构中的核心地位
人工智能·架构
武子康3 小时前
Java-80 深入浅出 RPC Dubbo 动态服务降级:从雪崩防护到配置中心秒级生效
java·分布式·后端·spring·微服务·rpc·dubbo
舒一笑4 小时前
我的开源项目-PandaCoder迎来史诗级大更新啦
后端·程序员·intellij idea
@昵称不存在5 小时前
Flask input 和datalist结合
后端·python·flask
zhuyasen5 小时前
Go 分布式任务和定时任务太难?sasynq 让异步任务从未如此简单
后端·go
东林牧之6 小时前
Django+celery异步:拿来即用,可移植性高
后端·python·django
超浪的晨6 小时前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
Pomelo_刘金6 小时前
用 DDD 把「闹钟」需求一点点捏出来
架构·rust·领域驱动设计
AntBlack7 小时前
从小不学好 ,影刀 + ddddocr 实现图片验证码认证自动化
后端·python·计算机视觉