想象你走进一家开了三十年的老餐厅。客人看到的是雅致的装潢、精致的餐具、热情的迎宾,但真正让这家店经久不衰的,不是这些,而是后厨里那本历经三任主厨、被翻得卷了边的菜谱——酱汁的配方、火候的章法、食材搭配的次序,全写在那里面。有一天餐厅决定重新装修、换上全套新式餐具,只要菜谱还在,老客人尝一口就知道:店没变。整洁架构(Clean Architecture)正是把这套经营智慧提炼成了软件世界的法则:让系统核心——业务规则——像那本菜谱一样稳定、独立、可传承;让外壳——框架、数据库、界面——像装修和餐具一样,随时可换而不伤筋骨。

岗位关联

对 AI 从业者而言,理解整洁架构意味着你设计的模型服务、数据管线、业务系统不再被某个框架绑架,能低成本适配模型迭代与基础设施变迁——这是架构功力最直接的体现。

目录
  1. 可视化演示
  2. 依赖规则
  3. 四层结构
  4. 依赖反转
  5. 架构关联
  6. 边界划分
  7. 工程落地
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示整洁架构四层同心圆结构、依赖反转原则DIP机制,以及与六边形/洋葱架构的对比,揭示分层架构的核心设计原则。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+_captions.layers+'

依赖规则

餐厅老板在后厨与前台之间画了一条看不见的“责任线”。前台想加新菜,不能直接闯进后厨改菜谱,只能把需求写成单子递进去;后厨做菜,也从不去研究客人用的是木筷还是银叉。整个餐厅只容许一件事自然发生:装饰、餐具、装修,都是围绕菜品而存在的,菜品从不迁就装修。

整洁架构把这条线叫做依赖规则(Dependency Rule)。它规定:源代码里的依赖关系,必须从外层指向内层。所谓外层,是靠近基础设施的东西;所谓内层,是靠近业务本质的东西。依赖永远不会反向,就像菜谱永远不会规定装修风格。

这条规则带来两个直接结果。第一,内层完全不认识外层:实体层的代码里不会出现 import 数据库驱动、不会出现框架注解。第二,外层可以自由更换:想换数据库、换 Web 框架,只要新外壳仍依赖内层定义的接口,内层纹丝不动。这也正是整洁架构让系统“可测试、可维护”的根源——测试时把外层替换成替身即可。

依赖规则的意义,就是那家老店的经营哲学:装修队换来换去,菜谱一页未动。把“会变的”和“不变的”分开,让变化止步于边界,让核心长治久安。

依赖方向: 框架层→适配器层→用例层→实体层\displaystyle \mathrm{依赖方向}:\ \mathrm{框架层} \to \mathrm{适配器层} \to \mathrm{用例层} \to \mathrm{实体层}
这个式子描述了依赖的“单向通道”:箭头永远从外框指向内核。
  1. 四层梯队从框架层到实体层一共四层,越靠右越接近业务核心,就像从装修到菜谱的距离依次缩短。
    框架层, 适配器层, 用例层, 实体层\mathrm{框架层},\ \mathrm{适配器层},\ \mathrm{用例层},\ \mathrm{实体层}
  2. 单向箭头每个箭头代表“依赖”而非“调用”,只允许从左指向右。
    A→B: A 依赖 BA \to B:\ A\text{ 依赖 } B
  3. 方向约束任何反向依赖都违反规则,如同菜谱不能反过来规定装修风格。
    反向依赖⇒违规\text{反向依赖} \Rightarrow \text{违规}

四层结构

一家餐厅能运转,靠的是四样东西:代代相传的菜谱、会做菜的厨师、传菜口与菜单翻译、以及墙上的装潢和桌上的餐具。这四样各司其职,缺一不可。整洁架构把系统也拆成对应的四层,由内向外依次为:实体层(Entities)、用例层(Use Cases)、接口适配器层(Interface Adapters)、框架与驱动层(Frameworks & Drivers)。

实体层封装最核心的业务规则,比如“订单金额等于单价乘以数量”“折扣不得超过五成”;它不关心数据存哪、页面长什么样。用例层编排一个个具体业务场景,比如“提交订单”要完成校验库存、计算金额、生成订单号——它像厨师按菜谱走完一道菜的流程,只认菜谱,不认客人。

接口适配器层负责翻译:把 HTTP 请求译成用例能懂的调用、把用例的返回值译成前端需要的 JSON,数据库的实现、消息队列的封装都在这一层。框架与驱动层则是壳:Spring、Django、数据库连接器、UI 组件,它们离业务最远,却最容易被替换。老板想换一批餐具,餐厅第二天就能换好。

四层结构最大的价值是“隔离”:菜谱不会因为餐厅换了两盏灯而改写。越往内越接近本质,越往外越接近噪音——这份隔离,让修改变得局部化、让替换变得低成本。

系统=框架层⊃适配器层⊃用例层⊃实体层\displaystyle \mathrm{系统} = \mathrm{框架层} \supset \mathrm{适配器层} \supset \mathrm{用例层} \supset \mathrm{实体层}
外层是包含内层的容器,越外层越靠近变化,越内层越靠近永恒。
  1. 包含关系符号表示外层嵌套并包含内层,四层像俄罗斯套娃。
    A⊃B: A 包含 BA \supset B:\ A\text{ 包含 } B
  2. 职责递减越往内职责越纯粹、越稳定;越往外职责越具体、越易变。
    外层: 多变→内层: 稳定\text{外层: 多变} \quad \to \quad \text{内层: 稳定}

依赖反转

想换装潢很简单,但“让后厨不被某套餐具绑架”需要一条妙计:让所有想合作的前厅都按同一张“下单卡”来点菜,这张卡由后厨设计,前厅照着执行。餐具品牌可以换,但下单卡的格式是统一的——后厨定义了规则,而不是被规则定义。

这正是依赖反转原则(DIP, Dependency Inversion Principle)。传统写法里,高层用例直接依赖低层实现,比如用例直接调用 MySQLRepository;反转之后,用例依赖一个自己定义的接口(下单卡),数据库实现反过来依赖这个接口。依赖的源头被扭转了:不再是从上到下依赖细节,而是上下两层同时看向一个稳定的抽象。

关键点在于抽象接口的归属:接口必须归内层所有。外部世界不是“提供接口给你用”,而是“按照你定义的接口来实现”。于是控制权反转了——不是内层迁就外层,而是外层迎合内层。就像客户来餐厅吃饭,不是客人教厨师做菜,而是客人按菜单点菜,菜单由餐厅定。

所以“用了接口”不等于依赖反转,接口站在哪一边才是关键。接口属于内层,才是真正的“后厨说了算”。

A高层用例⟶Interface⟵B低层实现\displaystyle A_{\text{高层用例}} \longrightarrow \text{Interface} \longleftarrow B_{\text{低层实现}}
高层的用例和低层的实现都面向同一个接口,接口归高层所有。
  1. 双方指向接口高层与低层不再直接相联,而是共同依赖中间的抽象层。
    A⟶Interface⟵BA \longrightarrow \text{Interface} \longleftarrow B
  2. 接口归属内层接口定义权在高层用例手里,低层只是实现者。
    Interface 归 A,B 实现 Interface\text{Interface}\ \text{归}\ A,\quad B\ \text{实现}\ \text{Interface}

架构关联

老店经营久了,老板发现别家店也用类似的法子:有人把餐厅搬进玻璃房子,四周通透,每个出入口都设一个传菜口;有人搭起洋葱般的帐篷,一层护着一层,越往里越神秘。这就是软件界两个著名的同类架构:六边形架构(Hexagonal Architecture)与洋葱架构(Onion Architecture)。

六边形架构强调“端口与适配器”:核心业务在中央,四周的端口定义对外能力,适配器把外部世界接进端口——好比玻璃房四面各设传菜口,任何方向的客人按同一规则点单。洋葱架构则画出一圈圈由内向外的依赖边界,依赖只允许从外圈指向内圈——好比帐篷的布料一层裹一层,你想碰到核心,得先穿过所有外层。

它们与整洁架构其实是同门师兄弟:都主张核心业务不依赖基础设施,都通过边界机制切断反向依赖,只是绘图方式与命名颗粒度不同。六边形重“端口适配”,洋葱重“层次包裹”,整洁架构重“分层加规则”——三者描述的是同一种世界观。下一次听到“我用的是洋葱架构所以不用整洁架构”,你就知道这说法站不住脚。

它们不是互相竞争的关系,而是同一理念的三张面孔。理解了这一点,读书、面试、选型时都能少一分纠结,多一分通透。

六边形架构≅洋葱架构≅整洁架构\displaystyle \text{六边形架构} \cong \text{洋葱架构} \cong \text{整洁架构}
三者同构:核心稳定、边界适配、依赖向内。
  1. 同构关系符号表示本质相同,都是“核心隔离 + 边界适配”的变体。
    X≅Y: X 与 Y 结构相同X \cong Y:\ X\text{ 与 } Y\text{ 结构相同}
  2. 视角差异同一种理念在不同图纸上呈现不同侧重,但依赖规则一致。
    六边形:端口洋葱:圈层整洁:分层\text{六边形}:\text{端口}\quad \text{洋葱}:\text{圈层}\quad \text{整洁}:\text{分层}

边界划分

传菜口是餐厅里最神奇的一扇门:食物只能从后厨端出去,点单纸却可以从外面递进来。门只有一个,东西却可以双向流动——但规矩只有一条:菜谱永远不许出门。软件系统的边界,正是这样的单向门。

数据流永远是双向的:点击“提交订单”,请求从外层(Controller)流入内层(用例层);订单创建成功后,响应又从内层流向外层。但依赖流是单向的:永远从外层指向内层。我们可以说“边界”是数据双向穿行、依赖单向通行的门禁。

实践中,跨越边界时用接口作为“门框”。外层调用内层公开的接口,内层对外层一无所知;Java 里可能是 interface,Python 里可能是 ABC 抽象类,目的都是同一件事:让依赖在边界处止步。很多项目死在看似无关紧要的“穿透”上:实体直接序列化成数据库记录、用例层 throw 出 SQLException——边界一旦被穿透,菜谱就上了餐桌,看似方便,实则核心失守。

边界一旦守住,内外就能各自演进:内层专心业务,外层尽情折腾。门只有一扇,规矩只有一条——菜谱不离后厨,依赖永不外泄。

数据流∈{进入, 流出},依赖流: 单向向内\displaystyle \text{数据流} \in \{\text{进入},\ \text{流出}\},\qquad \text{依赖流}:\ \text{单向向内}
数据可以进出两个方向,依赖却只有一个方向:向内。
  1. 数据流集合数据是双向的,既有请求进入也有响应流出,如同点单与上菜。
    数据流∈{进入, 流出}\text{数据流} \in \{\text{进入},\ \text{流出}\}
  2. 依赖单向性依赖流则被严格限制为单向向内,这是边界最核心的纪律。
    依赖流: 单向向内\text{依赖流}:\ \text{单向向内}

工程落地

老板决定开连锁店了。他把所有资源按职责分成四间库房:菜谱库、操作手册库、翻译工具库、装修物料库。项目结构一清二楚,任何新店照此复制就能开业。工程落地的第一步,就是这种“模块划分”。

对应到代码工程里,常见的划分是:domain 模块放实体与核心业务规则,application 模块放用例与内层接口定义,adapter 模块放接口的各类实现(数据库、消息队列、Web Controller),infrastructure 模块放框架配置与启动类。模块间的依赖方向严格沿 infrastructure → adapter → application → domain 单向流动,谁也不能反向引用。

实施顺序讲究“先内后外”:先写实体、再写用例、定义接口,最后接框架。就像先定菜谱、再定流程、最后选装修——而不是先订装修材料再倒推菜谱。测试时,外层全部用替身(mock/stub)代替,核心逻辑的单测不启动 Spring 容器、不连数据库,跑得飞快。落地不必一步到位,先在一个业务模块划出边界、打通依赖方向,再逐步推广到整个系统。

守住边界,架构自然会变得可维护;先内而外,系统才不会沦为“一堆框架的拼盘”。就像开分店,菜谱定好,装修随意换,家家都还是那家老店。

modules={domain, application, adapter, infrastructure}\displaystyle \mathrm{modules} = \{\mathrm{domain},\ \mathrm{application},\ \mathrm{adapter},\ \mathrm{infrastructure}\}
四个模块对应四种职责,依赖顺序从外壳步步深入内核。
  1. 四元组划分四个模块代表从核心到外壳的四种角色,职责互不重叠。
    domain, application, adapter, infrastructure\mathrm{domain},\ \mathrm{application},\ \mathrm{adapter},\ \mathrm{infrastructure}
  2. 依赖链模块间只允许沿固定方向依赖,任何一个反向引用都是坏味道。
    infra→adapter→app→domain\mathrm{infra} \to \mathrm{adapter} \to \mathrm{app} \to \mathrm{domain}

AI 应用场景

AI推理服务的模型仓抽象

阶段:模型部署与推理链路

解决的问题:传统推理服务把模型加载逻辑与业务代码搅在一起,换推理框架(TensorFlow→ONNX)就要改业务代码,风险大、回归多。

具体案例:某智能客服团队将“获取模型”抽象为内层接口,外层分别实现 TensorFlow 与 ONNX 两个适配器。模型升级时只替换适配器,核心的会话编排逻辑一行未动,上线耗时从两天缩到两小时。

数据管道的存储引擎切换

阶段:数据处理与特征工程

解决的问题:数据清洗逻辑总被存储 API 绑架,从 Hive 换到 ClickHouse 就像重写一遍管道。

具体案例:某特征平台把“读取特征”“写入特征”定义为领域接口,Hive 实现与 ClickHouse 实现并列部署,上层特征计算用例零改动完成切换——如同餐厅换了食材供应商,菜谱一字未改。

遗留系统重构的突围路径

阶段:系统现代化改造

解决的问题:老系统耦合严重,改一行影响全局,团队不敢动也不敢不乱。

具体案例:某银行核心系统重构,先抽出“利率计算”等核心规则放入实体层,再用用例层编排流程、用适配器对接老接口,最后分阶段替换外围模块。每步都可回滚,核心规则在重构中保持稳定。

Python 演示

python54 行
from abc import ABC, abstractmethodfrom dataclasses import dataclassfrom typing import List # 实体层: 领域核心对象, 不依赖任何框架@dataclassclass OrderItem:    name: str    price: float    quantity: int     def total(self) -> float:        return self.price * self.quantity @dataclassclass Order:    items: List[OrderItem] # 用例层: 编排业务场景, 只面向接口编程class CalculateOrderTotal:    def __init__(self, tax_provider):        self.tax_provider = tax_provider     def execute(self, order: Order) -> float:        subtotal = sum(item.total() for item in order.items)        tax_rate = self.tax_provider.get_rate()        return subtotal * (1 + tax_rate) # 接口适配器层: 接口归用例层所有, 外部实现它class TaxRateProvider(ABC):    @abstractmethod    def get_rate(self) -> float:        pass class SimpleTaxProvider(TaxRateProvider):    def get_rate(self) -> float:        return 0.06 class ZeroTaxProvider(TaxRateProvider):    def get_rate(self) -> float:        return 0.0 # 框架层: 组装所有组件, 提供运行入口def main():    order = Order([        OrderItem("coffee", 28, 2),        OrderItem("cake", 42, 1),    ])    app = CalculateOrderTotal(SimpleTaxProvider())    total = app.execute(order)    print(f"total: {total:.2f}") if __name__ == "__main__":    main()
目的:演示整洁架构四层的最小闭环——实体定义业务规则、用例编排流程、接口由核心定义、外部实现被注入组装。
  1. class OrderItem实体层对象,把“单价×数量”这条业务规则收在领域内部,不掺任何数据库或框架代码。
  2. class CalculateOrderTotal用例层,负责“计算订单总额”这一场景的编排,只认识接口不认识具体实现。
  3. class TaxRateProvider(ABC)抽象接口定义在用例一侧,而非外部框架一侧,这正是依赖反转的关键。
  4. class SimpleTaxProvider外层实现,随时可替换成别的税率策略,就像换一批餐具不影响菜谱。
  5. CalculateOrderTotal(SimpleTaxProvider())在框架层完成组装,依赖通过构造函数注入,核心代码完全不感知具体策略。

常见误区

误区我把代码分成了 Controller、Service、DAO 几个文件夹,目录看起来整整齐齐,这应该就是整洁架构了吧?依赖方向什么的,先不管,目录像就行。

正解目录只是外壳,整洁架构的灵魂是依赖方向。Controller 可以依赖 Service,但 Service 绝不能反过来依赖 Controller;任何一层直接引用内层细节,分层就名存实亡。就像把菜谱整整齐齐放在厨房抽屉里,可厨师每天点外卖——抽屉再整齐,也改变不了后厨失守的事实。

为何常见:人眼最先看到的是文件结构,目录最容易模仿;而依赖方向藏在代码关系里,肉眼难察,于是“看起来分层”被误当成“做到了分层”。

误区实体层不就是 ORM 里的那些 Model 类吗?一张表对应一个类,字段一致、使用方便,省得再写一套对象。

正解实体是业务规则与核心数据结构的载体,比如“订单”是业务概念;ORM 模型是持久化机制的一部分,属于适配器层。把两者混为一谈,等于把菜谱和食材库存单当成同一个东西——菜谱讲怎么做,库存单讲存多少,使命完全不同。实体层一旦贴满 ORM 注解,换数据库就变成改写菜谱。

为何常见:Django、Spring Data 等框架把数据库模型直接暴露为业务对象,初学者天天和它打交道,很容易视为一体。

误区我在 Service 里注入了 Repository 接口,这总该是依赖反转了吧?接口不都用了嘛。

正解还要追问接口的“产权”归谁。如果 Repository 接口定义在 ORM 框架包里、由数据库框架给出,Service 依然在依赖框架;只有当接口声明在业务侧、由外层去实现时,才算真正反转。接口站在内层,才是后厨写下单卡;接口站在外层,只是前厅在发号施令。

为何常见:“面向接口编程”深入人心,但很少有人追问接口由谁定义、由谁拥有,于是误以为“用了接口”就等于“反转了依赖”。

误区六边形架构、洋葱架构、整洁架构是三套互相竞争的设计,用洋葱就不能提六边形,选型时必须站队。

正解三者是同一理念下的不同画法:六边形强调端口与适配器,洋葱强调依赖圈层,整洁架构强调四层加规则。真实项目里往往同时体现三者特征,谈不上互斥。选哪张图取决于你想强调哪个维度,而不是架构对错。

为何常见:市面教程常把三种架构分开讲、对比着讲,读者就容易把它们想象成擂台上的对手,而忽略了它们血脉相通。

面试题

Q1. 整洁架构中,源代码依赖的唯一合法方向是什么?

单选题 · easy

  • A.外层指向内层← 正确答案
  • B.内层指向外层
  • C.同层之间互指
  • D.任意方向均可

答案:A。依赖必须从外层指向内层,内层完全不感知外层的存在。就像后厨做菜不研究客人用木筷还是银叉。

Q2. 整洁架构四层结构里,最内层是哪一层?

单选题 · easy

  • A.框架与驱动层
  • B.接口适配器层
  • C.业务用例层
  • D.核心实体层← 正确答案

答案:D。实体层是最内层,承载核心业务规则,相当于后厨那本代代相传的菜谱。

Q3. 用例层(Use Cases)的主要职责是什么?

单选题 · easy

  • A.管理数据库连接池
  • B.编排单一业务场景← 正确答案
  • C.渲染用户的页面
  • D.配置依赖的容器

答案:B。用例层负责编排一次业务场景:校验、计算、调用实体。就像厨师按菜谱完成一道菜,不关心食材从哪里来。

Q4. 依赖反转原则中,高层模块应当直接依赖什么?

单选题 · mid

  • A.低层模块的具体类
  • B.低层模块的父类
  • C.高层的抽象接口← 正确答案
  • D.框架提供的基础库

答案:C。高层定义接口、低层实现接口,这才叫反转。管事的(后厨)负责写下单卡,跑腿的(前厅)按单办事。

Q5. 以下哪一项通常属于接口适配器层?

单选题 · mid

  • A.订单聚合根实体
  • B.存取款业务用例
  • C.控制器和网关← 正确答案
  • D.网络协议栈层

答案:C。Controller、Gateway 负责把外部数据翻译成用例能懂的形式,属于适配器层,如同传菜口把客人的话转成下单卡。

Q6. 以下哪种做法违反了依赖规则?

单选题 · mid

  • A.用例层引用实体层
  • B.适配器层引用用例层
  • C.实体引用数据库驱动← 正确答案
  • D.框架层引用适配器层

答案:C。实体层是最核心的业务层,引用数据库驱动等于菜谱依赖某品牌食用油,一换油菜谱就废了。

Q7. 关于六边形架构与整洁架构的关系,正确的是?

单选题 · mid

  • A.两者完全相互对立
  • B.六边形是其简化版
  • C.同源理念不同表达← 正确答案
  • D.整洁架构升级版本

答案:C。六边形、洋葱、整洁架构都是“核心隔离+边界适配”的表达,区别只是画图和命名的角度。

Q8. 跨越架构边界时,哪些做法符合整洁架构理念?

多选题 · hard

  • A.使用内层定义的接口← 正确答案
  • B.依赖注入提供实现← 正确答案
  • C.实体序列化数据库记录
  • D.用例层捕获SQL异常

答案:AB。A 和 B 守住边界:接口归内层、实现由外层注入。C 和 D 让内层感知数据库细节,穿透边界,破坏单向依赖。

Q9. 实体层(Entities)中应该包含哪些内容?

多选题 · hard

  • A.核心业务规则逻辑← 正确答案
  • B.关键业务数据结构← 正确答案
  • C.ORM注解与序列化
  • D.HTTP请求响应对象

答案:AB。实体只承载业务规则与核心数据结构。ORM 注解、序列化、HTTP 对象都属于外层知识,掺入实体层就污染了核心。

Q10. 关于洋葱架构与整洁架构的说法,正确的有哪些?

多选题 · extreme

  • A.洋葱强调依赖由外向内← 正确答案
  • B.洋葱层数可多可少← 正确答案
  • C.整洁禁用一切接口
  • D.核心不依赖框架部件← 正确答案

答案:ABD。洋葱架构的依赖方向由外向内,层数可根据系统复杂度增减;整洁架构同样要求核心不依赖框架。但整洁架构不禁止接口——恰恰相反,它靠接口来守边界,所以 C 错误。