Logo
Section 1 / 3

数据流

让我们来了解整洁架构中的数据流。

导入

数据流

让我们来了解整洁架构中的数据流。

在本章中,我们将介绍一个采用整洁架构(Clean Architecture)设计的简单系统。本章的目的是让我们熟悉整洁架构中的核心概念,例如关注点分离(Separation of Concerns)和控制反转(Inversion of Control),这两个概念在系统设计中都至关重要。

在描述系统中数据流动的过程中,我们会刻意省略一些实现细节,以便专注于整体架构思想,而不必过多关注具体实现。我们将在后续章节中再次深入分析这个示例,并解释为何做出某些特定的设计选择。目前,我们的重点是理解整体结构。

本课程中,我们选择设计一个简单的 Web 应用程序,用于提供房间租赁服务。我们将其命名为 Rent-o-Matic。该应用的访问地址为 http://www.rentomatic.com。

现在假设一位用户想要查看 Rent-o-Matic 上可租用的房间。他们在浏览器中打开应用,并通过网站菜单找到列出公司所有出租房间的页面。我们假设该页面的 URL 为 /rooms?status=available。当用户的浏览器访问此 URL 时,会向我们的系统发送一条 HTTP 请求。系统中有一个专门等待并处理 HTTP 连接的组件,称为 Web 框架。

Web 框架

Web 框架的作用是解析 HTTP 请求,并获取生成响应所需的数据。在本例中,请求包含两个关键部分:端点本身(/rooms)以及一个查询参数(status=available)。端点 相当于对系统的命令。当用户访问某个端点时,就相当于向系统发出信号,请求某项特定服务——在此场景下,即获取所有可租房间的列表。

用例(Use Case)

Web 框架基于 HTTP 协议工作。当它解析完请求后,会将相关信息传递给另一个负责处理这些信息的组件,这个组件被称为 用例。用例是整个整洁架构中最关键的组件之一,因为它承载了业务逻辑。

业务逻辑 是我们为了提供某种服务而定义的具体算法或流程,用于对数据进行转换和处理。在构建系统时,我们通常已经明确系统如何为用户创造价值,因此据此设计相应的业务逻辑。这一点非常重要,因为它决定了数据应如何被处理、提取和呈现。以下是一些典型的业务逻辑示例:

  • 搜索引擎根据查询关键词查找相关网页;
  • 社交网络展示用户所关注的人发布的动态,并按特定算法排序;
  • 旅游平台推荐从地点 A 到地点 B 的最佳旅行方案。

用例实现了业务逻辑中的某一具体功能。在本例中,我们有一个“查找可用房间”的用例,其依据正是 status 参数提供的值。这意味着该用例需要获取公司管理的所有房间,并筛选出状态为“available”的房间。

有人可能会问:为什么不由 Web 框架直接完成这项任务?答案在于良好系统架构的一个重要特征:关注点分离。也就是说,不同的职责和领域应当彼此独立。Web 框架的职责是处理 HTTP 协议,由专注于该模块的开发人员维护。如果我们在其中加入业务逻辑,就会导致两种完全不同领域的代码混杂在一起,增加复杂性。

关注点分离 是一个需要牢记的重要概念:系统不同部分应各司其职。当两个独立的部分操作相同的数据或参与同一过程时,它们就形成了 耦合。尽管有时耦合难以避免,但组件间耦合度越高,修改其中一个就越容易影响另一个。因此,应尽可能避免不必要的耦合。

正如我们将看到的,分层设计能够显著降低系统维护成本,使各个模块更易于测试和替换。

回到我们的例子。系统中的用例需要获取所有可用房间的信息,这正是业务逻辑的体现。当前情况较为简单,仅涉及基于属性值的过滤操作。但在其他场景中,业务逻辑可能更为复杂,例如基于推荐系统自动下单,这时用例可能需要与多个组件交互,而不仅仅是数据源。

存储系统

用例所需处理的数据必须存储在某个位置,我们称该组件为 存储系统。它可以是数据库(如关系型数据库),也可以是文件、网络接口或远程传感器等任何形式的数据源。只要能为用例提供数据,即可视为数据源。

在系统设计过程中,以 抽象 的方式思考问题至关重要。抽象与组件共同构成系统的基本构建块。组件在系统中扮演某种角色,与其具体实现无关。组件的抽象层级越高,细节越少。高层次的抽象无法直接解决实际问题,因此抽象设计最终仍需通过具体技术或解决方案来实现。

为简化说明,本例中我们使用 PostgreSQL 这类关系型数据库,因为大多数开发者对此较为熟悉。但需要注意的是,这并非唯一可行的数据存储方案。

用例如何与存储系统连接?如果我们在用例中硬编码调用特定系统(例如 SQL 语句),那么两者将产生强耦合。然而正如前文所述,这正是系统设计中应尽量避免的情况。强耦合意味着组件之间紧密绑定,修改一个往往迫使另一个也必须修改,同时也增加了测试难度,因为组件无法独立运行。当其中一个组件是数据库这类复杂系统时,这种限制会严重拖慢开发进度。

举例来说,假设我们的用例直接调用 Python 的 psycopg 库来访问 PostgreSQL 数据库,这就使得用例与特定数据源耦合在一起。一旦更换数据库,就必须修改代码。这显然不理想,因为用例包含的是核心业务逻辑,不应因底层数据库变更而改变。那些不包含业务逻辑的系统部分,应被视为实现细节。

Course Image

实现细节 是指非核心设计的技术或解决方案。这里的“细节”并不表示其复杂度低,实际上某些实现细节的复杂程度可能超过系统的核心部分。

相比 HTTP 端点,关系型数据库更加丰富和复杂;而 HTTP 端点又比一组有序对象更复杂。应用程序的核心是用例,而不是数据存储方式或访问机制。通常,实现细节更多关乎性能或可用性,而核心部分则专注于实现业务逻辑。

如何避免强耦合?一种简单有效的方法是使用 控制反转(Inversion of Control)。下一节将简要介绍这一技术。在本课程后续内容中,当我们实际应用控制反转时,还会进一步深入探讨。

控制反转

控制反转是一种用于降低系统组件间耦合度的技术。其核心思想是将组件封装起来,使其暴露统一的接口。调用方只需依赖该接口,无需了解底层具体实现。这样,组件便与接口耦合,而非与具体实现耦合。

控制反转分为两个阶段。第一阶段,被调用的对象(在本例中为数据库)被封装在一个标准接口之下。该接口定义了一组所有实现都共有的功能,并将这些功能映射到底层具体实现的语言调用上。

现实生活中的类比是电源插头。电器设备并不是为某一种特定插头设计的,而是适配符合特定规格的通用插口。例如,在英国购买的电视通常配备英标插头(BS 1363)。若没有配备,也可通过转接器连接本地插座。类似地,在控制反转中,我们将用例(电视)连接到未专为匹配接口设计的数据库(插座)。

在本课程的例子中,用例需要提取状态为 available 的所有房间,因此数据库封装层只需提供一个入口点。我们将这个入口点命名为 list_rooms_with_status。

第二阶段,调用方(即用例)需修改其实现方式,避免硬编码对具体实现的调用,从而解除耦合。用例在其构造函数中接收一个对象作为参数,并在创建时获得该适配器的具体实例。具体的实现方式取决于所使用的编程语言。Python 没有显式的接口语法,因此我们假设传入的对象实现了所需的方法。

现在,用例通过接口与适配器连接。它可以调用 list_rooms_with_status 入口点,并传入 available 状态,以查找网站上的可用房间。适配器了解存储系统的具体细节,因此能将方法调用和参数转化为针对具体数据源的一条或多条请求,并将结果转换成用例期望的格式返回,例如一个包含房间信息的字典列表。

此时,用例可根据需要继续执行其余的业务逻辑,并将处理结果返回给 Web 框架。

Web 框架将从用例接收到的数据转换为 HTTP 响应。在当前场景中,这是一个用户主动访问的页面端点,因此 Web 框架会在响应体中返回一个 HTML 页面。

如果该端点是由前端异步 JavaScript 代码调用的内部接口,则响应体更可能是 JSON 格式的数据结构。

开始学习

分层架构的优势