惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Comments on: Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
李成银的技术随笔
美团技术团队
博客园 - 三生石上(FineUI控件)
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
Jina AI
Jina AI
S
SegmentFault 最新的问题
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
E
Exploit-DB.com RSS Feed
J
Java Code Geeks
腾讯CDC
V
V2EX
NISL@THU
NISL@THU
M
MIT News - Artificial intelligence
量子位
T
Tor Project blog
T
Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - Franky
Scott Helme
Scott Helme
U
Unit 42
博客园 - 聂微东
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网
Vercel News
Vercel News
GbyAI
GbyAI
MyScale Blog
MyScale Blog
Microsoft Security Blog
Microsoft Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
有赞技术团队
有赞技术团队
W
WeLiveSecurity
T
Tailwind CSS Blog
S
Schneier on Security
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
I
Intezer
Last Week in AI
Last Week in AI
D
Darknet – Hacking Tools, Hacker News & Cyber Security

暗无天日

读:tetris-sql——用一条SQL查询实现俄罗斯方块 - 暗无天日 TIL: dired 里按时间标记文件——dired-mark-if 与夏令时陷阱 - 暗无天日 读:从端点到行动——面向 AI 代理的后端设计 - 暗无天日 读:Amin Bandali 与 Protesilaos 谈 Emacs 内置功能的深度定制 读:Clojure 世界的 AI 代理调教术——四个改变行为的 Skill TIL: minibuffer 激活时也能操作其他窗口 - 暗无天日 读:Tramp改了配置怎么不生效 - 暗无天日 读:为 project.el 写一个自定义后端 - 暗无天日 读:AI 时代的敏捷开发 - 暗无天日 TIL: 给 dired 异步命令加 nohup,让外部程序活过 Emacs 退出 TIL: elisp-fontify-semantically——让 Emacs 看懂你的 Elisp 代码 TIL-etags扫描外部库头文件 - 暗无天日 读:The Many Faces of flet——Elisp 局部函数的三种写法 读:df 与 du——为什么两个磁盘用量命令数字对不上 - 暗无天日 TIL-可观测性工具的成本盲区 - 暗无天日 TIL: 把 Emacs Buffer 打印成图片 读:gamegrid.el——Emacs 内置游戏是怎么写出来的 - 暗无天日 TIL: MCP 服务器不到 20 行 Python 就能写出来 TIL-AI 工具普及后的组织观察 - 暗无天日 读:当 Agent 开始写数据库——六个防御模式 - 暗无天日 读:右键菜单——Elisp 开发的隐藏利器 - 暗无天日 读:Git 仓库里的隐藏配置文件 - 暗无天日 读:20条软件工程定律 - 暗无天日 读:编译高性能 Emacs - 暗无天日 读:为什么我在终端里待了十年——一个 Emacs 用户的 GUI 观察 读:Event Sourcing——让你的数据库记住每一次变更 - 暗无天日 读:数据管道中Schema变更的四种形状 - 暗无天日 读:SES——Emacs内置的简易电子表格 - 暗无天日 TIL: 用 parallel 加速 rsync 迁移海量小文件 TIL:给 AI 一个更小的世界——技术选型的上下文窗口约束 - 暗无天日 读:emacs chat 技巧拾遗——从 bandali 的配置里捡到的那些技巧 读:当 Agent 成为生产调用者——四个被打破的运维假设 - 暗无天日 TIL: describe-personal-keybindings 查看你的自定义按键 - 暗无天日 读:Linux 创建指定大小文件的三种方式——dd、fallocate 与 truncate - 暗无天日 读:Yazi——在终端里管理文件的新选择 - 暗无天日 读:软件测试的反馈视角——CLEAR 原则从测试到运维 - 暗无天日 MobileOrg Android:从 API 17 迁移到 API 34 的实战记录 TIL:微服务与复杂度守恒——从单体到分布式的代价转移 - 暗无天日 读:MCP 时代的安全威胁——幻觉权限与三道防线 - 暗无天日 读:超越对话——用 Skills 和 Agents 工程化上下文 TIL:用 :box 给 mode-line 加内边距 org-mobile-push 卡顿排查实战:从黑盒到字节码反编译 - 暗无天日 读:用 LLM 重构遗留代码——三个陷阱与一套方法 - 暗无天日 读:AI 辅助编程的三种错误用法 - 暗无天日 读:Before GitHub - 暗无天日 读:AI in Software Architecture - 暗无天日 读:把成本当作 SLI - 暗无天日 TIL: 用进程树展开定位被脚本包装的 JVM 进程 - 暗无天日 读:Linux 删文件的真相——用 /proc 恢复被进程持有的已删除文件 - 暗无天日 读:Choosing a Python Logging Library in 2026 Emacs buffer 导出:五种方案对比 - 暗无天日 TIL: flymake 错误跳转加入 Evil 跳转列表 TIL: 用 Org-mode 列表管理选择题题库 - 暗无天日 读:sysstat 诊断链——从 sar 到 pidstat 的排查路径 读:理论靠谱,生产翻车的六个集成模式 - 暗无天日 读:双写问题——@Transactional 给不了的跨系统一致性 - 暗无天日 读:PostgreSQL 随机测试数据生成——从快速造数到自动化填充 - 暗无天日 读:逆萨丕尔-沃夫假说与编程语言 - 暗无天日 读:理解 MCP 架构——LLM 直接调 API 与 MCP 协议的对比 读:Emacs 连接数据库时密码放哪里 - 暗无天日 TIL:watch 命令的几个遗漏技巧 - 暗无天日 TIL:Python 3.15 的 sentinel() 内置函数 读:7 Techniques That Supercharged My Claude-Assisted Development 读:AI 编码代理的四种工作流 - 暗无天日 读:Agent 的瓶颈不在模型,在基础设施 - 暗无天日 读:EvoForge——用群体进化优化 AI Agent - 暗无天日 TIL:Google Stitch 的 DESIGN.md,给 AI 读的设计系统说明书 Emacs 批量搜索替换:从场景到命令 - 暗无天日 TIL: image-mode 的 header-line 中显示图片尺寸 dotfile仓库大扫除:清理过时的配置 - 暗无天日 读:The Art of Logging——日志规范清单 - 暗无天日 从CSS选择器到自然语言:网页自动化的两种范式与取舍框架 - 暗无天日 TIL-用 curl + w3m + awk 从 HTML 表格提取数据 读:Shell脚本安全编码的五条铁律 - 暗无天日 读:Emacs newcomers-presets theme —— 30+ 项新手预设一览 读:Protesilaos 的 Emacs 合理默认配置 —— 兼与 newcomers-presets 对比 控制 Bash 历史记录的 6 个场景 读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 读:LLM 生产环境六种失败原型——基准测试无法预测的那些故障 - 暗无天日 读:Prompt Injection 五层纵深防御——从输入过滤到审计追踪 - 暗无天日 读:为什么所有 Prompt Injection 防御都会被攻破——以及架构上该怎么办 - 暗无天日 读:JVM 后端性能调优备忘——从一次生产事故中学到的优化要点 - 暗无天日 读:Java 容器化——从 Fat JAR 到高效 Docker 镜像 读:整洁代码的几个通用原则——从 Go 生态看起 - 暗无天日 读:规则引擎——从 if-else 到业务规则管理 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日
TIL DDD战术模式:用Clojure让代码说人话 - 暗无天日
2026-05-24 · via 暗无天日

是什么

先花一分钟搞清楚 DDD 在说什么。

DDD(领域驱动设计)是一套软件设计方法论,核心观点是: 写代码之前,先搞清楚业务 。它分两层:

  • 战略 DDD :划定领域边界(这个系统管什么、不管什么),和业务专家定义一套统一语言,同一件事,代码里和会议上叫同一个名字
  • 战术 DDD :用具体的编码模式把业务理解落实到代码里。本文讲的 Entity、Value Object 这些,就是战术模式

战略先搞清楚"是什么",战术再回答"怎么写到代码里"。如果反过来,先设计数据库表再想业务,代码迟早变成一堆 getThing()setThing()

代码烂掉,绝大多数时候不是因为写得差。

真正的原因更简单:三个月后你打开那段代码,你已经不知道它当时在解决什么业务问题了。函数名叫 handle-alert ,但你不知道它是升级告警、静默告警还是只是打印日志。注释早就过期了,commit message 只写了"fix"。

DDD 的战术模式要解决的就是这个问题。它不关心你用了什么框架、数据库、消息队列,它只坚持一件事: 代码结构本身应该说出业务在做什么

为什么

(defn handle-alert [alert-id new-level]
  (let [alert (db/find :alerts alert-id)
        old-level (:level alert)]
    (when (should-escalate? old-level new-level)
      (db/update! :alerts alert-id {:level new-level})
      (notify-team (:team-id alert) new-level)
      (log/info "alert" alert-id "escalated from" old-level "to" new-level))))

此示例为概念示意,函数 db/findshould-escalate? 等未定义,不可直接执行。

代码能跑,但你一眼扫过去,脑子里全是问题:

  • "升级"规则是什么? should-escalate? 里的逻辑为什么长这样?
  • notify-teamdb/update! 为什么非得捆在一起?能不能只升级不发通知?
  • alert 那张 map 里到底有哪些字段?什么变了还算是同一条告警,什么变了就不再是?

这段代码没有 bug,SQL 也没毛病。问题是 业务意图在代码里找不到地方落地 。DDD 的战术模式就是给这些意图分配明确的"住处"。

怎么做

这里用告警系统的场景来展示八个模式。例子用 Clojure。

Entity:对象由身份定义

告警规则( AlertRule )是典型的 Entity,改名字、改阈值、改级别,只要 :id 不变,它就是同一条规则:

(defrecord AlertRule [id name query-expr level])

;; 同一条规则,:id 不变,name/level 改了也认它
(def cpu-rule (->AlertRule "r-cpu-001"  "CPU过高" "cpu>90"   :p1))
(def cpu-rule-v2 (->AlertRule "r-cpu-001" "CPU过高-改" "cpu>95" :p0))
cpu-rule id: r-cpu-001 name: CPU过高 level: :p1
cpu-rule-v2 id: r-cpu-001 name: CPU过高-改 level: :p0
Same id? true

defrecord 在这里的实际作用是给读代码的人一个信号:这东西有独立身份,不要用 assoc 随便改它。

Value Object:对象由值定义

告警级别 P0P1P2 不需要身份。只要值相同,它们就是同一个东西,典型的 Value Object。在 Clojure 里用 clojure.spec 约束:

(require '[clojure.spec.alpha :as s])

(s/def ::alert-level #{:p0 :p1 :p2 :p3})
:p1 valid? true
:p5 valid? false
:urgent valid? false

不用 spec 的时候,级别就是一个裸 keyword,你靠什么拦着别人写成 :p5 或者 :urgent ?spec 把"告警级别只能有这四个值"这条 业务规则 钉在代码里,而不是在开发者的脑子里。

Aggregate Root:访问聚合的唯一入口

告警通知组( AlertGroup )是一组告警规则的集合。要求是:外部不能直接捅进 :rules 字段里增删规则,必须走 add-rule / remove-rule 。这层控制不只是一个技术约束——=add-rule= 这个名字本身就是业务语言,它在说"往组里加规则"是一件合法的业务操作。以后加校验(比如一个组最多 10 条规则),只改这两个方法就行。

(defrecord AlertGroup [id name rules])

(defn add-rule [group rule]
  (update group :rules conj rule))

(defn remove-rule [group rule-id]
  (update group :rules #(remove (fn [r] (= (:id r) rule-id)) %)))

(defn get-rules [group]
  (:rules group))
After adds, rules count: 2
After remove r-001, rules count: 1
Remaining rule id: r-002

add-ruleremove-rule 现在可以在代码库里被 grep 到了。它们是有名字的业务操作,将来搜 add-rule 一眼就能看出谁在往通知组里加规则。

Domain Event:事件是领域的一等公民

告警升级不是一段逻辑,它是一个 已经发生的事实 。把它变成一个显式的 record,代码中发生升级的位置只管创建这个 record。至于创建之后谁来读取、读取之后做什么,是发通知、记统计、写审计日志都由关心它的模块各自处理,互不耦合。

(defrecord AlertEscalated [alert-id old-level new-level escalated-at group-id])

;; 升级逻辑只负责"产出事实"
(defn escalate [alert-rule new-level]
  (let [event (->AlertEscalated (:id alert-rule)
                                (:level alert-rule) new-level
                                (java.time.Instant/now)
                                (:group-id alert-rule))]
    ;; 更新状态
    ;; (publish-event! event) ; 依赖具体实现,此处仅示意发布位置
    event))
Event alert-id: r-001
Event old-level: :p1
Event new-level: :p0
Event escalated-at type: java.time.Instant
Event group-id: g-001

AlertEscalated 的存在本身就是文档:一看就知道"这个系统有告警升级这回事,并且升级时会记录哪些信息"。

Factory:封装创建逻辑

创建一条告警规则看起来只是 new AlertRule() ,但实际上要生成 ID、校验级别合法、可能还要设默认值。这些逻辑如果散落在各处,新手拷贝代码时就很容易漏掉校验。Factory 把创建规则集中起来进行管理:

(defn create-alert-rule [name expr level]
  {:pre [(s/valid? ::alert-level level)]}  ;; 业务校验:级别必须合法
  (->AlertRule (str (java.util.UUID/randomUUID)) name expr level))
Created rule id: da2eaeb7-... name: 磁盘告警 level: :p1

注意 :pre 那行。如果你传了个 :p5 进去,函数直接抛错,不会让一条非法规则进入系统。这条校验原来可能在 Controller 里写一次、在 Service 里又写一次,现在 Factory 替它们干了。

Repository:隔离领域与持久化

业务代码不需要知道数据存在 PostgreSQL、MongoDB 还是内存里。它只需要说"存这个组"或者"按 id 查这个组"。Repository 定义一个接口,实现细节隐藏:

(defprotocol AlertGroupRepository
  (save [this group] "持久化通知组")
  (find-by-id [this id] "按 id 查询通知组"))
Found group: 基础设施

上面这个 defprotocol 只是契约。测试时用内存实现,生产环境换成 Postgres,业务代码不用改。更关键的是, savefind-by-id 这两个方法名本身就是业务语言:你在存一个通知组、查一个通知组,不是在写 SQL。

Domain Service:无处可去的领域逻辑

"把一条规则从 P2 升级到 P0"这个操作只涉及规则本身,不关别的事。但有些逻辑天生跨多个对象。"把一条规则从 A 组迁移到 B 组"就同时涉及源组和目标组,这个方法放哪边都不对。Domain Service 用来收容这类无处安放的业务逻辑:

(defn escalate-alert [rule new-level]
  {:pre [(s/valid? ::alert-level new-level)]}
  (assoc rule :level new-level))

(defn transfer-rule [rule source-group dest-group]
  (->> source-group
       (remove-rule (:id rule))
       ((fn [src] [(add-rule dest-group rule) src]))))
Original level: :p2 → escalated level: :p0

escalate-alerttransfer-rule 都是纯函数,输入规则,输出新状态。不碰数据库,不发消息,不调 API。这是 Domain Service 和 Application Service 的关键区别:Domain Service 只管业务规则本身——输入什么,输出什么。输出之后要不要存起来、要不要通知别人、要不要发消息,这些由 Application Service 来管。

Application Service:编排用例步骤

业务规则写好了、持久化接口有了、事件类型也定义了,谁来把它们串成一条完整的业务流程?Application Service 干的就是这个,它是指挥,不是执行者:

(defn handle-escalation [repo rule new-level]
  (let [escalated (escalate-alert rule new-level)         ;; 1. 执行业务规则
        group (find-by-id repo (:group-id rule))]
    (save repo (update-group-rule group escalated))       ;; 2. 持久化
    (publish! (->AlertEscalated (:id rule)                ;; 3. 发事件
                                (:level rule) new-level
                                (java.time.Instant/now)))
    escalated))
Published: AlertEscalated old-level: :p2, new-level: :p0

注意 handle-escalation 自己不包含任何业务规则,它只是把 Domain Service、Repository、Event 按顺序串起来。往后加审计日志或者加事务控制,改这一个函数就行,不用满项目找。

多说一句:DDD 的核心到底是什么

看完八个模式,一个自然的印象是:DDD 不就是用业务语言来给函数和方法起名吗?

这个直觉对,但不全对。DDD 实际上有三层:

第一层:语言一致。 函数叫 escalate-alert 而不叫 handle-alert ,方法名叫 add-rule 而不叫 update-list 。代码里的词就是会议上用的词。这一层 Clean Code 也在做,DDD 真正的穿透力在后面。

第二层:结构映射。 *Entity 和 Value Object 的区别不是命名习惯:Entity 有独立身份,改了属性还是它自己;Value Object 没有身份,值一样就是同一个东西。如果把一切东西都当 Entity(全都有 ID),或者全都用裸 map 不管有没有身份,你不会损失可读性,你损失的是 * 业务分类在代码层面的表达力

第三层:约束写进代码。 Factory 的 :pre 校验保证"级别只能是 P0-P3 这四个值",不经过 Factory 就创建不出一条非法规则。Aggregate 的 add-rule / remove-rule 保证外部永远只能走这两个入口修改通知组。这些约束原来在开发者脑子里、在团队约定的文档里、在 code review 的 checklist 里,DDD 把它们写进代码,不可绕过。

所以回到那个问题——"用业务语言表达函数和方法"是第一层,第二层是"用恰当的数据结构区分业务概念",第三层是"用代码边界强制执行业务规则"。三层加起来,让代码替你说出业务在干什么。