软件设计

关注点分离(SoC):解剖 800 行组件后的心得

你可能见过这种代码:所有内容都塞在一个 800 行的视图文件里。

3 分钟阅读
关注点分离(SoC):解剖 800 行组件后的心得 封面图

你可能见过这种代码:所有内容都塞在一个 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(缩小修改的影响范围)、最小惊讶(让行为符合预期),以及关注点分离(一次只做一件事)。

回头看,六条原则都指向同一件事:代码是给人读的,而不是给计算机读的;好的代码就是容易修改的代码。即使忘记原则的名字,也请记住这句话。