【引】你可能读过《clean architecture》一书, 也读过《clean code》,如果缺了些什么? 那可能就是 Clean API 了。本文译自“https://medium.com/perry-street-software-engineering/clean-api-architecture”。
在软件架构领域,网上讨论最广泛的架构之一是整洁架构(Clean Architecture)。它通过将项目划分为多个层级,实现关注点分离,从而提升代码的可维护性和可扩展性。
每一层都遵循单一职责原则,确保每个类只负责一部分逻辑,不仅使系统结构更清晰,也极大地方便了单元测试的编写与执行。
整洁架构的核心理念可以概括为:
依赖关系向内指向“业务核心”,外层可以依赖内层,但内层绝不可以反向依赖外层。
换句话说,没有哪一层可以看到比它更高层的细节。它们可以引用自己的子层,但从不允许跨层依赖或反向耦合。
这种设计思想不仅适用于整体系统架构,在具体场景如 API 开发中同样具有重要价值。那么,如何将 Clean Architecture 的理念应用到 API 端点的设计中?这就引出了“Clean API 架构”这一实践模式:
它将接口层、应用服务、领域逻辑和数据访问等模块清晰隔离,使 API 结构更加清晰、易于测试和长期演进。
在现代 Web 系统中,任何一个 API 请求通常都需要经过多个层级的处理——从负载均衡器、Web 服务器,到应用服务器,最终由具体的 API 或 Web 框架将请求路由到正确的代码路径。目前主流的开发框架如 Rails、Django 和 Spring Boot 等都提供了丰富的文档支持和成熟的生态体系,是大多数开发者首选的技术栈。
然而,当请求真正进入框架并开始业务逻辑处理后,不同系统的设计路径往往开始显著分化。
以 Rails 为例,其采用经典的 MVC(Model-View-Controller)架构,在小型项目中表现优异,结构清晰且上手成本低。但对于大型、高可用性的 API 系统而言,这种模式逐渐显露出局限性——控制器和模型容易膨胀为臃肿的“上帝类”,违背了单一职责原则,导致维护困难、测试复杂。
正因如此,在框架层级之下,我们所构建的整个系统设计更加注重解耦与可扩展性,并深受 Clean Architecture 的启发。这套设计理念强调业务逻辑应独立于外部依赖(如数据库、UI、框架等),从而提升系统的可测试性和长期可维护性。
在本系列后续内容中,我们将深入剖析我们是如何按照这一思想来组织各层级代码的。
在每一层中,我们会定义一个或多个单一职责的支持类,它们只服务于当前层级,不引用上下层的具体实现。这种严格隔离不仅有助于代码复用,也有效避免了层级混乱和过度耦合的问题。
此外,虽然我们在架构中使用了诸如 AWS EC2、SQS、RDS 和 ElastiCache 等云服务,但这些服务在我们的设计中被作为框架层的辅助工具类存在,而非核心业务逻辑的一部分。正如 Clean Architecture 所强调的那样:业务规则不应依赖于基础设施或数据存储方式,而应保持完全的独立。这也确保了我们的系统具备更强的可移植性与灵活性。
当一个请求穿越框架层,进入系统内部时,接口适配器层便开始发挥作用。这一层的核心职责是将外部输入转化为内部可理解的数据结构,并将应用逻辑的执行结果以合适的格式返回给调用者。
在这一过程中,控制器(Controller)扮演着协调者的角色。它首先通过 Request
对象提取请求参数,验证其语法格式,并完成用户身份认证等前置操作。随后,控制器实例化相应的业务类,驱动数据在不同层级之间的流转,从而启动真正的应用逻辑处理流程。
值得注意的是,控制器并不是接口适配器层中唯一负责业务流程的对象。我们还引入了 Jobs(任务),用于处理异步队列相关的操作——这部分内容将在后续章节中详细展开。
为了确保系统的清晰分层与职责分离,控制器依赖于多个辅助类:
负责检查输入数据的合法性,确保进入系统的信息符合预期格式;
专注于输出数据的格式化处理,为上层逻辑提供统一的数据视图;
则承担最终输出的封装工作,能够将数据转换为 JSON、HAML 或其他客户端可识别的格式返回。
此外,系统中还包含一种特殊的适配器——套接字中继类(Socket Relay),它通过 WebSocket 等通信通道,实时将状态变更推送给客户端,实现双向通信能力。
Request 类则是一个类型化的数据结构,聚合了当前请求所需的所有信息。与传统 HTTP 请求(通常是以键值对形式存在的 CGI 风格请求)不同,这种设计提供了更强的类型安全性和结构清晰性。
Response 类的功能类似于 Rails 中的渲染器,但它更加灵活,支持多种输出格式,如 HAML、JSON 或自定义类型,便于构建多端兼容的 API 响应。
最后,参数提取器(Parameter Extractor) 从原始的 params 散列中提取数据,并将其转换为正确的类型,如整数、浮点数或字符串,为后续逻辑提供强类型的输入保障。
整体而言,接口适配器层作为系统的“翻译官”,在外部请求与内部逻辑之间建立起高效、清晰的桥梁,是实现 Clean Architecture 分层思想的重要一环。
在 Clean API 架构中,应用逻辑层是整个系统真正开始处理业务需求的地方。它承接来自接口适配器层的请求,并协调数据验证、权限控制、外部调用以及最终的业务执行。
对于 GET 请求这类读取型端点,请求一旦进入该层,首先由服务类(Service)进行处理。服务对象负责确保输入参数的有效性,验证用户是否有权限访问目标资源,并通过 Repo(用于数据库操作) 或 Adapter(用于外部 API 调用) 从实体逻辑层获取所需数据。
在数据获取完成后,服务对象将结果封装为一个由 Result
对象返回。这种设计不仅统一了成功与失败的返回结构,也便于上层(如控制器)根据结果类型做出相应的响应决策。
而对于 POST、PUT 和 DELETE 等写入型请求,应用逻辑的处理流程类似,但引入了异步机制以提升性能和可靠性。服务对象仍然负责验证输入、授权用户,并准备写入所需的数据。不同之处在于,这些变更操作会被包装并提交到我们的任务队列(基于 Amazon SQS)中排队,交由后台的作业(Job)或异步服务来执行真正的数据写入操作。这种方式既减轻了主流程的压力,也增强了系统的容错能力和可扩展性。
此外,作业还承担着触发副作用的职责。例如,在数据持久化完成之后,作业可以通过 Relay 模块向客户端发送 WebSocket 消息,实时通知状态变更,实现前后端之间的即时反馈。
值得一提的是,在本架构中,Service 类还会组合一组专门的 Validator 类,对请求内容进行语义级别的验证。这意味着我们在系统中构建了双层验证机制:
发生在请求层,确保传入的数据格式正确;
则在应用逻辑层进行,确保数据在业务规则下是合理且合法的。
这种分层验证策略显著提升了系统的健壮性,避免了无效或非法数据对核心业务逻辑造成干扰,同时也使代码更具可测试性和可维护性。
实体逻辑层(Entity Logic Layer) 是系统中最具通用性和复用价值的部分。它不仅服务于当前 API 端点,也为其他多个接口和业务流程提供基础能力支撑。这一层承载了系统的核心业务规则以及与外部存储系统的交互逻辑。
在这一层级中,我们实现对持久化数据库(如 MySQL 或 PostgreSQL)的访问,封装了数据的读取、写入和转换逻辑;同时,Adapter 类 则负责对接各类外部服务 API,例如 AWS 提供的 S3(对象存储)、ElastiCache(缓存服务)等,使得系统能够灵活集成多种基础设施资源。
与上层(如应用逻辑层)中为特定端点定制的服务类不同,实体逻辑层中的类设计强调高内聚、低耦合与广泛复用性。它们通常不依赖于具体的请求或业务场景,而是围绕领域模型构建稳定的数据访问和业务处理能力。
简而言之,实体逻辑层是整个 Clean API 架构中最接近“不变”的部分——它屏蔽了外部变化的影响,确保系统核心逻辑稳定可靠,同时也为上层模块提供了统一、可测试、可替换的数据交互接口。
数据层(Data Layer) 是整个 Clean API 架构中最底层的一环,其核心职责是为上层模块提供统一的数据访问接口,并屏蔽具体存储实现的细节。理想情况下,这一层应保持高度简洁和可替换,专注于连接数据库、缓存、文件系统或其他持久化机制。
为了实现跨平台一致性,我们在不同技术栈中(例如 Android 或 iOS 开发)也为数据存储层建立了统一接口。通过依赖注入(Dependency Injection) 技术,我们可以在测试时轻松替换真实的数据源为内存中的模拟实现(Mock)。例如,在本地运行单元测试时,可以使用基于 SQLite 的内存数据库代替实际的文件系统或远程服务,从而提高测试效率并减少外部依赖的影响。
在 Web 服务器环境中,我们通常将云服务(如 Redis、Memcached、MySQL 等)抽象为单例对象,并根据部署环境动态指向不同的实际资源。例如,在开发阶段,这些服务可以指向本地运行的 Docker 容器;而在生产环境中,则连接真实的云服务实例。
支撑数据层的各类存储系统——如 MySQL 和 Postgres——通常以进程级别的单例形式存在,并通过依赖注入或配置管理进行初始化和替换。像 ActiveRecord 这样的 ORM 会维护自己的连接池,而 Redis 和 Memcached 等服务也需要类似的全局访问控制机制来管理连接资源。
对于基于 HTTP 的无状态服务(如 S3、DynamoDB 等),我们通常采用模拟双(Instance Doubles)或覆盖连接参数的方式来隔离外部环境。这使得测试过程更加可控,同时也能保证代码逻辑在不同环境下的一致性。
总之,数据层不仅是系统与外部世界交互的桥梁,更是实现可测试性、可维护性和可扩展性的关键所在。通过良好的抽象设计与灵活的注入策略,它确保了我们的业务逻辑不受底层存储细节的牵制,真正做到“一次编写,多环境运行”。
为了更好地理解各层架构的实际应用,我们来看一个最基础的 API 示例:将文件添加到收藏夹。即使在这样一个看似简单的操作中,每一层的设计理念依然得到了体现。
假设我们要创建一个允许用户将某个文件添加到其收藏夹的功能。在这个过程中,尽管表面上看只需要一个简单的数据库操作,但实际上,这个功能隐式地依赖于我们之前定义的每一层架构。
请求(Request)
请求参数直接从 HTTP 请求中提取,包含 target_id
(目标文件的ID)和 creator_id
(执行该操作的用户ID)。这些未经处理的原始参数构成了一个隐式的请求对象。
Params: { "target_id": 123, "creator_id": 456 }
控制器(Controller)
由于此场景下没有复杂的验证或表示逻辑需求,因此无需专门编写控制器代码。这意味着我们可以跳过这一步骤,直接进入服务层处理业务逻辑。
服务(Service)
服务层在此处承担了主要职责,它接收来自请求的 target_id
和 creator_id
,并查找或创建相应的领域对象 Favorite
。这一过程确保了输入的有效性和用户的授权状态,并协调后续的数据处理步骤。
实体逻辑(Entity Logic)
实体逻辑层负责与持久化存储交互,这里使用了一个特殊的 ActiveRecord 方法 first_or_create
来检查是否存在符合条件的记录,若不存在则创建新记录。这种方法不仅简化了数据访问逻辑,还保证了数据的一致性。
数据(Data)
在数据层,Favorite
模型充当了与底层数据库交互的角色,提供了对特定对象的操作接口。它封装了所有与数据库相关的细节,如连接管理、查询构建等,使得上层代码可以专注于业务逻辑而非技术实现。
通过这个简单的例子可以看出,即使是看似微不足道的功能,Clean API 的分层设计也能够提供清晰的结构划分,确保每个部分专注于自己的职责。这样的设计虽然初看起来可能显得有些复杂,但它极大地提高了代码的可维护性、测试性和扩展性。随着系统规模的增长,这种架构的优势将会更加明显。
如果对API 的设计、运维和演进感兴趣的话, 推荐阅读好友张力强和范怿平的译作——《精通API架构》!
【关联阅读】
没有被了解的API?一个老码农眼中的API世界
性能约定:API 限速
API协议设计的10种技术
API设计中性能提升的10个建议
API的性能约定
IOT语义互操作性之API接口
面向接口/协议?看DuerOS的技能开发
硬/软件接口:走向何方
架构设计过程中的10点体会
浏览器架构的温故知新
全栈必备:系统架构设计的10个思维实验
机器学习系统架构的10个要素
软件架构的10个质量属性
回顾Bob大叔的简洁架构
万字揭秘:生成式AI浪潮中的架构模式
解读六边形架构
再谈《全栈架构师》一文