想象你是一位米其林主厨,镇店之宝是一道法式洋葱汤。汤的灵魂不是浓稠的汤面,而是最中心那片被千万层外皮包裹、历经十二小时慢炖的洋葱芯。外层的皮随时可以更换——供应商换了,你换一批新皮;摆盘改了,你换个盘子。只要芯的配方不乱,这锅汤就永远是那锅汤。 软件系统也是这样。数据库、消息队列、第三方SDK、Web框架——这些全是洋葱的“皮”。真正值钱的“芯”是业务规则:订单折扣怎么算、风控阈值是多少、库存扣减顺序如何。可很多系统的毛病恰恰是芯被“皮”绑架了——业务逻辑散落在 DAO、Controller 和 Service 里,一换数据库就要重构半个系统。 洋葱架构(Onion Architecture)就是来解决这个问题的:它把系统画成一层层同心圆,最内层是领域模型与业务规则,外层只能依赖内层。依赖方向被强制反转,核心逻辑对数据库、框架保持“不闻不问”。我们把这颗洋葱切开,一层层讲明白。
对 AI 从业者而言,架构是模型落地的骨架。掌握洋葱架构,你就能把推理逻辑从框架和数据源中解放出来,让算法核心在 Docker、GPU 集群或业务系统之间从容迁移。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
领域为王:芯决定汤的味道
洋葱最贵重的永远是芯。一颗洋葱,剥掉干皮、掰开外瓣,真正决定气味层次的是中心那片嫩芯。厨师的刀法、火候、高汤,全在围着这颗芯转。软件系统也一样,业务规则就是那颗芯——订单算价的规则、风控拦截的条件、推荐排序的权重,它们构成了系统存在的理由。在洋葱架构里,最内层的领域模型就是这颗芯,它由实体、值对象、聚合、领域服务以及仓储接口共同组成。
为什么只能由“芯”来定义规则,而不是数据库?因为规则是业务的语言,是 AI 模型要表达的目标,而数据库只是储存规则的仓库。你换一种数据库,业务规则不该改动一个字。洋葱架构把领域模型放在圆心,同时立下一条铁律:任何外层代码都不得反向依赖领域层,领域层不 import 任何框架、任何 ORM、任何中间件 SDK。这样,系统的“味道”就只由芯决定。
如果用一句话概括依赖方向,可以写成:
切洋葱时,你的刀从外往里走,越靠近芯越小心。架构上也是一样,每一次改动都应从外向里谨慎推进,刀锋永远对向基础设施层,而不是核心。保住芯,汤的味道就稳了。
- ∀ 全称量词读作“对于任意”,它保证下面的规则对系统里每一个软件元素都生效,没有任何例外。
- x ∈ L_i表示元素 x 被归入编号为 i 的那一层,层号从内向外变大。
- dep(x, y)定义一种二元关系,读作“x 依赖 y”,既包含编译期的 import,也包含运行时的调用。
- ⇒ 蕴含如果左边的条件成立,右边的结论必然成立,它是规则的形式化推导逻辑。
- i ≥ j被依赖方的层号不大于依赖方,说明内层永远是被保护的,内层不反过来依赖外层。
层级划分:一圈一圈包起来
主厨处理洋葱时,会先把干皮撕掉,再去除外层老瓣,剩下最嫩的部分才入锅。每一层都有自己的角色:外皮负责保护,内瓣负责提供风味。洋葱架构也是按职责一层层包起来的,典型的四层划分从外到内依次是:基础设施层、应用服务层、领域服务层、领域模型。
最外层的基础设施层装着数据库访问、消息队列、邮件发送、第三方 SDK,这些都是“可替换的皮”。紧贴它的应用服务层编排用户请求、管理事务边界,相当于后厨的调度台。再往里的领域服务层处理跨实体的业务规则,比如“下单时同时校验库存与风控”。最内层的领域模型则是纯粹的业务对象,不夹杂任何技术噪音。每一层只掌握自己该知道的,外圈不知道内圈的实现,内圈也看不见外圈的存在。
这四层的关系可以用一个依赖链表达:
现实中,让应用服务层看不到 MySQL 的存在并不难:你只需要定义仓储接口放在领域层,把实现丢在外面。剥开第一层皮,里面永远是一个更干净的内瓣;剥到中心,就是你不想再动的梦幻配方。
- L_Infra 基础设施层最外层,负责与技术组件打交道,例如 MySQL 驱动、Redis 连接、HTTP 客户端。
- → 依赖箭头表示箭头左侧的层在编译期能看见箭头右侧的层,能 import 其中的类型。
- L_App 应用服务层它依赖更内层的接口,而不是直接依赖具体实现类。
- L_DomainSvc 领域服务层承担多个聚合之间的业务协作逻辑,依然只面向抽象。
- L_DomainModel 领域模型层链条终点,谁都不依赖它,它却决定整个系统的业务含义。
依赖反转:让供应商来适应你
主厨不会因为某家食材供应商送不来小洋葱,就改写自己的秘方。他会反过来规定:我要的洋葱必须是紫皮、直径 8 厘米、水分低于 15%,你们谁能供应我就跟谁合作。在洋葱架构里,这条“主厨标准”就是抽象接口。依赖倒置原则说的是:高层业务模块不该依赖低层技术实现,两者都应该依赖抽象,而抽象由领域层自己定义。
想象一个订单系统:应用服务不需要认识“MySQLOrderRepository“或”MongoOrderRepository“,它只需要认识领域层的 OrderRepository 接口。至于这个接口是用 MySQL 实现还是用内存列表实现,由最外层的组装者决定。这种”决定权在外部、标准在内部”的协作方式,就是依赖注入容器存在的意义——在应用启动时,把具体的实现用胶水组装进接口。
形式化地表达依赖倒置,就是两个模块同时面向抽象:
这样一套组合下来,主厨永远掌握“我要什么”的权力,供应商只负责“怎么满足”。以后就算把 MySQL 换成 PostgreSQL,业务层代码一行都不用改——供应商换了,配方没变。
- A_high 高级模块代表贴近业务的模块,比如结算应用服务,它关心“如何完成结算”。
- → 指向抽象高级模块在编译期只 import 抽象接口,不 import 任何具体技术类。
- I_abstract 抽象接口接口定义放在领域层,因此内层拥有对“规格”的话语权。
- ← 反向箭头低级模块反过来也指向同一抽象,实现类通过 implements 关系适配接口。
- A_low 低级模块容器、驱动、持久化实现等,它们不再被高层直接 import,而是被动态注入。
对比三层:汉堡与洋葱的差别
传统三层架构像一只汉堡:展示层、业务层、数据层从上到下叠成一座塔。好吃吗?好吃。但中层肉饼一旦位移,整只汉堡就散了。数据层一旦更换数据库,依赖它的业务层就要跟着改,连展示层都可能被波及。分层确实存在,但方向是“上层依赖下层”,数据像地基一样压在最下面,业务逻辑被地基的形状绑架了。
洋葱架构则完全不同。它不是三片面包垂直叠加,而是一圈一圈水平包裹。业务核心在中心,数据库、UI、外部服务都在外围。对比两种结构,最本质的差异在于耦合对象:三层架构说“UI 驱动一切”,数据表结构决定业务代码;洋葱架构说“领域驱动一切”,业务规则决定外围长什么样。前者的数据流是从外向内直插到底,后者的依赖则是每一圈只认更内圈。
用耦合度来量化两种架构对核心的影响:
汉堡好吃但怕塌,洋葱耐存放且能一层层剥开。遇到业务演进时,三层架构常常要重写肉饼;洋葱架构只需要把新需求翻译成内层的规则,再在外层补一个新端口。架构的灵活性,就藏在这种“芯稳皮可变”的结构里。
- I 耦合度衡量两块代码之间相互影响程度的指标,数值越大越容易牵一发动全身。
- (Domain, Infra)括号里写出被考察的两个对象——领域模型与基础设施。
- = 0 等于零表示两者之间不存在直接依赖关系,这是洋葱架构刻意设计的目标。
可测性:先尝芯,再上菜
一道好汤出锅前,主厨会用小勺单独尝一口汤芯,确认咸淡后才会注入容器端给客人。他不会等到整锅端上桌、客人动勺了才发现问题——那太晚了。洋葱架构最大的红利,就是你不需要启动数据库、不需要拉起消息队列,就可以直接品尝业务逻辑。因为领域层是纯的,它不依赖任何基础设施,测试时可以像操作普通函数一样验证规则。
这就带来一个关键的实践:外围依赖全部替换成 Mock 替身。测试订单算价时,不用真的连 MySQL,而是注入一个“内存版仓储”。被测系统只认 OrderRepository 接口,根本不在乎背后是数据库还是字典。单元测试因此变得飞快,几百条用例跑完只要几十毫秒,开发者可以每改一行代码立刻跑一遍,形成秒级反馈循环。
这种替换的合法性可以用等价式描述:
正因为这种等价性,测试金字塔才能落地:底层是大量廉价快速的单元测试,顶层是少量昂贵缓慢的集成测试。主厨先尝芯,确认味道再上桌;开发者先单测核心,再谈环境搭建。汤的稳定,是靠每一次小抿尝出来的。
- SUT 被测系统Software Under Test,即被测的领域服务或应用服务,是测试的主角。
- + 组合符号表示把被测对象与其协作组件装配到一起,形成完整的运行上下文。
- Mock(P) 模拟端口P 代表外围端口,Mock 是它的替身实现,比如内存仓储。
- ≡ 语义等价两条装配路径在“被测系统可正常协作”这一层面效果相等。
- Real(P) 真实端口真实实现仅用于集成测试阶段验证,单元测试阶段用不到它。
DDD配合:一整个洋葱由小鳞瓣构成
一整颗洋葱剥开后,你会发现它不是一个密实的球,而是由许多小鳞瓣层层环抱组成的。每个鳞瓣都有自己的薄皮、自己的汁液,凑在一起才是完整的风味。领域驱动设计(DDD)中的限界上下文就像这些鳞瓣:订单上下文、支付上下文、库存上下文,各自是独立的小洋葱,内部构建自己的领域模型和语言,彼此之间通过明确接口协作。
在洋葱架构里实现 DDD,最关键的是把聚合根放进领域模型的圆心。聚合根是保证一致性的边界,比如“订单”这个聚合负责维护订单项的不变量,任何对订单项的修改都必须穿过它。同时,仓储接口也归领域层所有,它表达的是“我需要一个能存订单的地方”,至于存到哪里,交给外层实现。这种安排让领域事件的发布也变得干净——事件在领域模型中产生,由应用服务转发给消息队列,事件的“语义”与“传输”就此解耦。
多个限界上下文之间,要遵守一条拓扑约束:
一个大型系统,就是由这些互相独立又协同的鳞瓣组成的洋葱球。每一瓣都有自己严密的芯,瓣与瓣之间通过防腐层交换信息。只有当每颗小洋葱都靠领域规则而不是数据库结构来定义时,整个系统才会像一颗真正的洋葱那样,层层分明、久放不坏。
- BC_i 第 i 个限界上下文一个自洽的业务边界,比如订单域或支付域。
- ∩ 交集两个上下文中共享的概念集合、实体定义交集。
- = ∅ 等于空集表示两边的模型不应共享同一个实体类,各自维护演进。
- i ≠ j 下标不同只对不同上下文进行比较,同一个上下文内部不适用此约束。
AI 应用场景
支付网关重构
阶段:模型服务与业务系统集成阶段
解决的问题:传统代码在 Service 里直接调用支付宝 SDK,导致算法分成计算逻辑与支付 SDK 强耦合。换支付渠道或升级 SDK 时,业务代码被迫跟着改,风险极高。洋葱架构把“分成计算”放入领域层,定义 PaymentGateway 接口作为端口,让支付宝、微信、银联的适配器都实现同一个接口。
具体案例:某电商平台重构结算系统,领域层只负责计算“平台抽成比例”“商家结算金额”等规则;外层的支付宝适配器与银联适配器通过依赖注入按配置切换。上线后切换支付渠道只需修改最外层配置,领域规则一行未动,参与重构的工程师反馈“改动面比预想缩小了 90%”。
AI推荐策略引擎
阶段:推理服务特征获取与策略编排阶段
解决的问题:推荐系统往往把特征拼接、模型推理、结果重排混在同一个类里,导致特征源从 Redis 换成特征平台时,整个算法流程都要重测。洋葱架构把“召回→排序→重排”的规则放进领域层,用 FeatureSource 接口屏蔽特征来源,外层的 Redis 适配器与离线特征适配器可随意替换。
具体案例:某资讯推荐团队用洋葱架构重构策略引擎,领域层纯 Python 实现重排逻辑,不依赖任何存储 SDK。业务方给重排逻辑增加“相似内容去重”规则,只改领域层并补充单元测试,两天的重构一周内就稳定上线。
订单域微服务拆分
阶段:业务规则数字化建模阶段
解决的问题:单体应用拆分成微服务时,最大的痛点是订单、库存、支付模型互相嵌套,剪不断理还乱。洋葱架构配合 DDD 的限界上下文,把每个业务域做成一个小洋葱,通过防腐层隔离外加服务交互,保证各自的领域模型不互相污染。
具体案例:某零售企业拆分订单中台,订单域、库存域、支付域各建独立洋葱,领域事件通过消息中间件解耦。拆分后,促销活动的算价逻辑收敛在订单域的核心层,库存域的 AI 缺货预测模型只消费库存事件,互不干扰。
Python 演示
# 洋葱架构极简演示:订单折扣计算# 领域层:定义实体/值对象/仓储接口from abc import ABC, abstractmethodfrom dataclasses import dataclass, field @dataclassclass LineItem: sku: str price: float qty: int @dataclassclass Order: id: str items: list = field(default_factory=list) def total(self) -> float: return sum(i.price * i.qty for i in self.items) class OrderRepository(ABC): @abstractmethod def save(self, order: Order) -> None: ... @abstractmethod def find(self, order_id: str) -> Order: ... class PricingService: # 领域服务:只关心业务规则,不认识数据库 def apply_discount(self, order: Order, rate: float) -> float: raw = order.total() return round(raw * rate, 2) # 应用层:编排用例,管理事务边界class OrderAppService: def __init__(self, repo: OrderRepository, pricing: PricingService): self._repo = repo self._pricing = pricing def checkout(self, order: Order, discount_rate: float) -> float: final = self._pricing.apply_discount(order, discount_rate) self._repo.save(order) return final # 基础设施层:实现接口,放在最外层class MemoryOrderRepository(OrderRepository): def __init__(self): self._db = {} def save(self, order: Order) -> None: self._db[order.id] = order def find(self, order_id: str) -> Order: return self._db[order_id] if __name__ == "__main__": repo = MemoryOrderRepository() pricing = PricingService() service = OrderAppService(repo, pricing) order = Order("A001", [LineItem("KEYB", 99.0, 2), LineItem("MOUSE", 45.0, 1)]) print("Final total:", service.checkout(order, 0.8))
class OrderRepository(ABC)接口定义在领域层,规定“我能存订单”这个能力,具体实现留待外层给出。class PricingService领域服务是纯业务逻辑,不 import 任何存储或框架,可以原封不动地放进单元测试。class OrderAppService应用服务只编排检查和保存流程,自身不知道 MySQL 或 Redis 的存在。class MemoryOrderRepository(OrderRepository)基础设施层作为最外层实现接口,将来换数据库只需新增一个类。service = OrderAppService(repo, pricing)在入口处完成依赖注入,把具体实现装配进抽象接口,这就是洋葱的“组装点”。
常见误区
误区我看洋葱架构有四个层,那我干脆分七层八个层,看起来更“洋葱”,代码一定更高级。层数越多越能体现架构功力。
正解层数本身不是目的,依赖方向才是。洋葱架构的每一层都意味着一次间接调用,层数过多会让最简单的请求穿过重重包装,代码难读、调试困难。分层的价值在于“该隔离的变化被隔离”,而不是“层越多越干净”。就像一个洋葱,皮太多反而说明芯不新鲜。
为何常见:初学者容易把“分层”等同于“架构好”,看到示意图上的多个同心圆就想往上叠层数,忽略了每多一层带来的理解成本。
误区不就是把原来的 Controller、Service、DAO 改成叫“应用层”“领域层”“基础设施层”吗?包名换一换,我一天就能改造完。
正解三层架构的依赖方向是“上→下”,数据层在最底部支撑一切;洋葱架构的依赖方向是“外→内”,核心是最中心的领域模型。命名改变背后是关注点的迁移:三层让你盯着数据表设计类,洋葱逼着你先想清楚业务规则。如果只是平移包名而没有反转依赖,新架构立刻会退化成旧架构。
为何常见:因为多数人对“分层”的直观印象来自三层架构,看到洋葱也有层就认为二者是同构的,忽略了架构真正的灵魂——依赖方向。
误区我用的 ORM 那么好用,直接把数据库表映射的实体当领域模型不就行了?字段注解写在上面,查询懒加载也方便,少写一堆代码。
正解一旦领域模型被打上 ORM 注解,它就失去了“纯粹性”。数据库表结构的变化会直接侵入业务规则,而且 ORM 的延迟加载、会话管理也会悄悄渗入领域层。洋葱架构要求领域模型不依赖任何框架,ORM 实体属于基础设施层,领域层只定义自己的接口和纯对象,两者之间通过适配器转换。
为何常见:项目一开就引入 ORM,实体和表结构天然长得像,很多人不到半年就忘了“仓储接口隔离”这层设计,直接把 ORM 拖进了核心。
误区反正 Redis 缓存、消息队列都是系统的一部分,它们的配置、工具类、连接池也可以放在核心包里,反正都在同一个项目里。
正解洋葱架构强调“边界”不是物理上的包,而是编译期的依赖方向。基础设施一旦进入核心包,就会诱导其他核心代码依赖它,让规则和实现纠缠不清。正确做法是:核心包只声明端口(接口),基础设施作为外环实现,通过依赖注入在启动时接入。把连接池放进核心,等于把菜市场的熙攘搬进了厨房操作间。
为何常见:项目规模小的时候,基础设施和业务代码混在一起也能跑,但当连接方式或中间件升级时,混放带来的改动成本才会爆发出来。
误区洋葱架构强调领域层为核心,那我干脆把所有逻辑都写进领域服务,反正领域越大越“核心”。
正解领域层应该承载“业务规则”,比如折扣计算、库存校验;而“用例编排”(先扣库存再发消息、事务边界怎么管理)属于应用服务层的职责。把用例塞进领域层,会让领域模型背上太多它不该知道的技术流程。精确的切分让每一层都只做好一件事,这也是 DDD 中“应用服务薄、领域服务厚”的经验。
为何常见:开发者理解了“核心优先”后容易矫枉过正,把所有代码都往中心推,忘了洋葱架构同样强调“各层各司其职”。
面试题
Q1. 洋葱架构中,依赖方向的总原则是什么?
- A.内层可以随意依赖外层
- B.外层依赖指向内层← 正确答案
- C.各层之间双向依赖
- D.所有层依赖同一数据库
答案:B。洋葱架构的核心铁律是外层依赖内层,内层绝不反向依赖外层。选项 A 反向错误;选项 C 的双向依赖会破坏架构边界;选项 D 描述的是数据驱动理念,与洋葱架构相悖。
Q2. 领域模型在洋葱架构中位于哪一层?
- A.最外层基础设施
- B.中间应用服务层
- C.最内层核心位置← 正确答案
- D.与数据库处于同一层
答案:C。领域模型是洋葱架构的圆心,承载实体、值对象和业务规则。A 和 D 把领域模型与基础设施混为一谈,B 把它下放到应用层,都违背了“核心最内”的设计原则。
Q3. 更换数据库厂商时,洋葱架构设计良好的系统应如何处理?
- A.修改领域模型中的实体
- B.替换外层仓储实现类← 正确答案
- C.重写全部应用服务逻辑
- D.调整领域服务中的算价规则
答案:B。洋葱架构中仓储接口定义在领域层,具体实现位于外层。更换数据库只需替换外层实现,领域模型、应用服务无需改动。A、C、D 都会破坏核心层的稳定性。
Q4. 依赖倒置原则在洋葱架构中的正确表现是?
- A.高层模块依赖低层实现
- B.所有模块依赖抽象接口← 正确答案
- C.低层模块依赖高层模块
- D.接口随低层实现定义
答案:B。依赖倒置要求高级业务模块与低级技术模块都依赖抽象接口,且接口归属领域层。A 是传统三层写法,C 方向反了,D 让抽象跟着实现走,都违背 DIP 的语义。
Q5. 洋葱架构对比传统三层架构,最本质的改进是什么?
- A.减少代码文件数量
- B.业务规则不被基础设施绑架← 正确答案
- C.让数据库查询速度更快
- D.消灭所有接口定义
答案:B。三层架构是数据驱动,业务层依赖数据层;洋葱架构把领域模型放在核心,基础设施在外部,业务规则因此不被数据库和框架绑架。A、C 不是架构设计的目标,D 与依赖反转背道而驰。
Q6. 关于领域层与测试的关系,描述最准确的是?
- A.必须启动数据库才能测试
- B.纯业务逻辑可脱离基础设施进行单元测试← 正确答案
- C.测试时必须真实调用第三方支付
- D.领域层不需要测试,只需测 Controller
答案:B。洋葱架构让领域层不依赖基础设施,因此可以用内存替身直接验证纯业务逻辑。A 和 C 是传统集成测试的做法,D 把测试重心放错位置,核心规则才是最需要测试的部分。
Q7. 领域层中出现哪种内容时,说明架构出现“泄漏”?
- A.仓储接口抽象定义
- B.ORM 注解和数据库连接← 正确答案
- C.聚合根实体设计
- D.值对象定义
答案:B。OBM 注解、数据库连接、HTTP 客户端都属于基础设施关注点,出现在领域层即架构泄漏。A、C、D 都是领域层的合法成员,分别对应端口、聚合根与值对象。
Q8. 两个限界上下文之间共享同一个订单实体类,可能引发什么问题?
- A.代码文件数量增加
- B.一方修改导致另一方崩溃← 正确答案
- C.数据库表自动迁移
- D.依赖注入失效
答案:B。限界上下文要求模型互不重叠。共享实体相当于让两个上下文共享同一种语义,任何一方的规则演进都会波及另一方,破坏边界稳定性。A 与架构无关,C、D 是具体技术问题,不是模型共享的直接后果。
Q9. 洋葱架构提升可测试性的原因有哪些?
- A.领域核心与基础设施解耦← 正确答案
- B.测试用例必须连真实数据库
- C.核心逻辑可独立运行验证← 正确答案
- D.用替身对象替代外部依赖← 正确答案
答案:ACD。A 描述了解耦带来的测试便利,C 说明核心可独立执行,D 是 Mock 替身的实践方式。B 说反了——真实数据库属于集成测试阶段,不是单元测试的前提条件。
Q10. 洋葱架构配合 DDD 战略设计的正确实践有哪些?
- A.用限界上下文划分独立小洋葱← 正确答案
- B.在领域层定义聚合根与仓储接口← 正确答案
- C.让基础设施依赖领域端口定义← 正确答案
- D.用 ORM 直接操作聚合完成持久化
答案:ABC。A 把每个限界上下文做成独立洋葱,B 让聚合根与仓储接口占据核心层,C 是依赖反转的体现。D 违背洋葱架构——聚合的持久化必须经过仓储接口,ORM 不能直接渗入领域层。