你可能见过这种代码:所有内容都塞在一个 800 行的视图文件里。
负责绘制界面的视图代码里有 API 调用,旁边是折扣率计算,下面是日期格式化函数,中间还夹着分析事件发送。想修改任何一处,都必须读完全部 800 行。
这类文件违反的原则就是关注点分离(Separation of Concerns)。开发原则系列的最后一篇,讲的就是这个原则。
关注点分离(SoC)是指将程序划分为不同的关注点单元,让每个部分只处理一个关注点。
“关注点”是程序需要处理的任务类型。如何显示界面、从哪里获取数据、如何应用业务规则,都是不同的关注点。
这个术语由计算机科学家 Dijkstra 提出,他在 1974 年的文章中这样描述。
一次只专注于思考一个方面。这是我所知道的唯一有效的思考方法。
人的大脑无法同时处理多个关注点。所以,代码也应该让每个片段只包含一个关注点。
从熟悉的案例开始:HTML、CSS、JS
如果你做过 Web 开发,就说明你已经每天都在使用关注点分离。
| 技术 | 关注点 |
|---|---|
| HTML | 结构 — 有什么 |
| CSS | 表现 — 看起来如何 |
| JavaScript | 行为 — 如何响应 |
如果像过去一样在 HTML 标签中堆满 style 属性和 onclick,就连修改一个按钮的颜色也要翻遍 HTML。拆成三个文件后,设计师只看 CSS,负责标记的人只看 HTML。
后端的控制器—服务—仓库结构,以及 iOS 中将视图、视图模型和数据层分开,也都是同一原理。MVC 和分层架构归根结底都是将关注点分离形式化的模式。
来解剖刚才那个 800 行视图
我们按关注点拆分前面看到的 800 行视图。
// Before: 所有关注点都在一个文件里
struct ProductPage: View {
// 关注点 1: 获取数据(网络调用、加载、错误处理 150 行)
// 关注点 2: 业务规则(折扣率、库存计算 200 行)
// 关注点 3: 分析事件(点击跟踪 100 行)
// 关注点 4: 绘制界面 (body 350 行)
}
// After: 让每个关注点都有自己的归属
ProductStore(id:) // 获取数据
calcDiscount(product, user) // 业务规则(纯函数)
Tracker.track("product") // 分析事件
struct ProductPage: View { // 只绘制界面
@State private var store: ProductStore
var body: some View {
let price = calcDiscount(store.product, user)
...
}
}
拆分之后,优点很明显。折扣策略变化时,只需查看 calcDiscount;而且这个函数与界面无关,是纯函数,因此也容易测试。API 规范变化时,只需修改 ProductStore。相比必须读完 800 行,修改范围缩小到十分之一。
应该拆分到什么程度?
关注点分离最难的是“那么,边界应该画在哪里”。拆得过细,就会变成在几十个文件之间来回阅读的迷宫。
我的标准是修改的原因。这和 SOLID 篇中讨论 SRP 时的问题相同。
这两段代码会因为相同的原因、在相同的时间发生变化吗?
折扣策略和界面布局变化的原因不同,所以要分开。相反,按钮和它的点击处理器几乎总是一起变化,因此应该放在一起。从这个角度看,SwiftUI 按视图单元将结构、样式和行为集中在一个文件中,并不矛盾。它是按会一起变化的单元,而不是技术类型,重新划分关注点的。
系列收尾
关于关注点分离,最需要记住的是这一点。
- 让每段代码只处理一个关注点。人的大脑无法进行多任务处理。
- 在实践中,应按“会一起变化的单元”而不是技术类型来划分边界。
- 过度拆分会制造迷宫。拆分本身不能成为目的。
至此,开发原则系列告一段落:KISS(保持简单)、DRY(让知识集中在一个地方)、YAGNI(只实现当前需要的内容)、SOLID(缩小修改的影响范围)、最小惊讶(让行为符合预期),以及关注点分离(一次只做一件事)。
回头看,六条原则都指向同一件事:代码是给人读的,而不是给计算机读的;好的代码就是容易修改的代码。即使忘记原则的名字,也请记住这句话。

