最近读到喵神的《编译器,靠你了!使用类型改善状态设计》,深受启发。这篇文章让我意识到,我在设计 RaAPIWrapper 时,为了解决一些工程问题而采用的方案,在不经意间与“类型即状态”的设计思想不谋而合。

我之前还没有正式介绍过这个库,所以借此机会,正式与大家分享 RaAPIWrapper 的设计,特别是它为了解决特定问题而采用的架构,以及这些决策在事后看来,是如何“碰巧”实现了更先进的设计模式。


RaAPIWrapper 的诞生

RaAPIWrapper 的设计初衷,源于对现有网络库 Moya 的反思,以及对 Android 路由框架 ARouter 设计模式的借鉴。

为何不直接用 Moya?

在 Swift 社区,Moya 是一个非常优秀的类型安全网络库,它通过 enumprotocol 的组合,解决了大量 API 管理中的问题。然而,在深度使用后,我们发现 Moya 的设计在某些方面存在优化空间。

Moya 的核心是 TargetType 协议,一个 API 的完整定义需要你在 enumswitch 中,为 path, method, task 等多个属性分别提供实现。这导致了一个问题:

一个 API 的核心信息(如请求方法、路径、参数)被分散在了多个代码块中。

当项目 API 数量增多时,开发者需要在一个庞大的 switch 语句中不断上下文切换和上下跳转,才能拼凑出一个 API 的完整样貌。这降低了代码的可读性和维护性,也是 RaAPIWrapper 希望解决的核心痛点。

不过需要强调的是,RaAPIWrapper 的目的并非全盘否定 Moya,当前也不具备替代 Moya 的能力。Moya 依然是一个功能强大的网络执行层,尤其在插件化、请求执行等领域。RaAPIWrapper 的设计初衷是专注于解决 API 定义的内聚性问题,它被设计为一个上层的“API 定义层”,可以与像 Moya 这样的成熟网络库协同工作,形成优势互补。

ARouter 是什么?

ARouter 是 Android 生态中一个著名的路由框架,由阿里巴巴开发并开源。该框架通过注解 (@Route) 的方式,让开发者能够以一种极其声明式和解耦的方式定义页面路径。在 README 中有如下的示例:

@Route(path = "/test/activity")
public class Test1Activity extends Activity { ... }

我非常欣赏这种将“路由”从具体的实现中抽离出来,变成一个集中、静态、易于管理的设计。

于是,一个想法在我脑中萌生:我能否将这种优雅的、基于注解的声明式语法,从“页面路由”领域迁移到“API 定义”领域?

这个想法,成为了 RaAPIWrapper 的起点。我的目标是创建一个网络层,它应该具备以下特质:

  • API 声明化:API 的定义应该像 ARouter 的路由一样,通过类似注解的语法附加到一个静态属性上,而不是散落在各个业务代码中去手动拼接 URL。

  • 集中化管理:所有单个 API 定义所需要的内容都应该被清晰地组织在一起,不会散落在多个地方,查找起来要非常方便。

  • 定义与实现解耦:API 的“定义”应该与其“如何被请求”完全分离。定义部分只关心 API 的元数据(路径、方法、参数结构),而底层的网络请求库(是 Alamofire 还是 URLSession)可以随时被替换,而不影响定义。


像定义路由一样定义 API

为了实现上述目标,我将 RaAPIWrapper 的核心架构设计为一个声明式的 API “地址簿”:

项目带有一个用于演示的 Playground,您可以通过该项目进一步了解本框架。

@GET("/api/v1/no_param")
static var noParamAPI: APIParameterBuilder<()>? = nil

@POST("/api/v1/tuple_param")
static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = .init {
    // `Dictionary` and `Array` can be used directly as parameters.
    ["id": $0.id, "name": $0.name]
}

@POST("/post")
static var postWithModel: APIParameterBuilder<Arg>? = .init {
    // When the parameter `Arg` complies with the `APIParameter` (`Encodable & Hashable`) protocol, 
    // it can be used directly as a parameter.
    $0
}

就像 ARouter 的 @Route("/path/to/page") 一样,最核心的信息应该在声明的那一刻就一目了然。这种方式的优势是显而易见的:

  1. 高度可读:你可以像 Moya 一样将多个接口定义在一个文件中,任何人打开这个文件,就能立刻了解模块中有哪些可用的 API。
  2. 高内聚性:您不用再通过多次跳转,才能获取到一个接口的全部信息,所有信息都定义在一起。
  3. 易于维护:修改、添加或废弃一个 API,都只需要在这个“地址簿”中进行,而不需要深入到业务逻辑中去寻找那些零散的 URL 字符串。
  4. 天然解耦@GET@POST 的定义,只包含了 API 的元信息。它本身并不关心网络请求是如何发出的。底层的实现被 RaAPIWrapper 的执行层所封装,这为后续替换网络库或增加统一的请求处理逻辑(如加密、加签)提供了极大的便利。

核心设计:一个声明式的 API 定义

在确立了“声明式、高内聚低耦合”这些核心设计理念之后,我需要寻找一些足够强大的技术手段来支撑它。@propertyWrapper 属性包装器与 Swift 的泛型系统正是实现这些理念的最佳方案。

最初我使用泛型,只是想找到一种方法来避免 switch 语句和增强代码的扩展性,但最终的成果却意外地与“类型即状态”思想相契合。这对我来说,也是一个在实践中学习和发现的过程。

@propertyWrapper 属性包装器

@GET@POST 是基于一个名为 API 的通用属性包装器(@propertyWrapper)构建的。

我们以 @POST 为例,它的定义非常简单,只是一个类型别名(typealias):

// file: Sources/Core/Wrapper/HTTPMethod/POST.swift

public enum PostHTTPMethod: APIHTTPMethodWrapper {
    public static var httpMethod: APIHTTPMethod { "POST" }
}

/// Encapsulates the data needed to request the `POST` api.
public typealias POST<Parameter> = API<Parameter, PostHTTPMethod>

真正的核心在于 API 这个类。它是一个泛型属性包装器,接受两个类型参数:Parameter 代表 API 的参数类型,HTTPMethod 则是一个遵循 APIHTTPMethodWrapper 协议的类型,用于提供具体的 HTTP 请求方法(如 "POST")。

// file: Sources/Core/Wrapper/API.swift

@propertyWrapper
public class API<Parameter, HTTPMethod: APIHTTPMethodWrapper> {

    public var wrappedValue: APIParameterBuilder<Parameter>?
    public let path: String

    // ... other properties

    public init(
        wrappedValue: APIParameterBuilder<Parameter>?,
        _ path: String,
        // ... other parameters
    ) {
        self.wrappedValue = wrappedValue
        self.path = path
        // ...
    }

    public var projectedValue: API<Parameter, HTTPMethod> { self }
}

@propertyWrapper 的设计要点在于:

  1. 初始化时捕获信息:当我们写下 @POST("/post") 时,Swift 编译器会调用 APIinit(wrappedValue:_:) 方法。路径 "/post" 作为参数被传入,并保存在 path 属性中。wrappedValue 则是我们赋给静态变量的 APIParameterBuilder 实例,它包含了参数的构建逻辑。
  2. 分离“定义”与“执行”API 包装器通过 projectedValue 暴露了自身。这意味着,当我们使用 $ 符号(如 BasicAPI.$postWithModel)时,我们获取到的是 API 这个包装器对象本身。这个对象已经包含了构建一个完整请求所需的所有信息:路径、HTTP 方法、以及参数构建器。这就实现了 API “定义”与“执行”的分离——定义时只关心元数据,执行时通过 $ 获取到一个可执行的请求对象。

通过这种方式,@propertyWrapper 不仅提供了声明式的语法,更在底层完成了信息的聚合与封装,是实现 API 高内聚设计的关键。

“类型驱动”思想

如果说 @propertyWrapper 提供了骨架,那么泛型系统则为这个骨架提供了核心的类型安全保障。如今回顾,我才意识到这个为解决扩展性问题而选择的方案,其本质恰好就是“类型即状态”思想的体现,并且这一思想同时运用在了 API 的两个泛型参数 HTTPMethodParameter 之上。

我们再次审视 API 的定义:API<Parameter, HTTPMethod: APIHTTPMethodWrapper>

首先,HTTPMethod 参数本身并不是一个字符串值(如 "POST"),而是一个类型。例如 PostHTTPMethod 这个类型,它的唯一作用就是通过遵循 APIHTTPMethodWrapper 协议来携带 "POST" 这个字符串。通过 typealias POST<P> = API<P, PostHTTPMethod>,我们将 POST 这个别名与 PostHTTPMethod 这个“状态类型”永久绑定。这样,请求方法就在类型层面被固定下来,杜绝了传入错误字符串的可能性。此外这种写法还有很多好处:

  • 利于扩展,可以很简单的定义多个 HTTP Method。
  • 通过 extension + where PostHTTPMethod = ...,可以针对不同的 HTTP Method 定义不同的请求方法。比如业务层面规定 GET 请求不允许携带参数,那么就可以定义不带参数的 request() 方法。
  • 在业务层需要获取到 HTTP Method 时,可以避免 switch case

其次,Parameter 参数以一种更动态的方式应用了同样的思想。观察 API 的定义:

// API with no parameters
@GET("/api/v1/no_param")
static var noParamAPI: APIParameterBuilder<()>? = nil

// API with parameters
@POST("/api/v1/tuple_param")
static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = .init { ... }

我们并没有在 @GET@POST 中显式指定 Parameter 的类型。这里的机制是 Swift 的类型推断

  • 当我们声明 static var noParamAPI: APIParameterBuilder<()>? 时,Swift 编译器会分析变量的类型。它看到 APIParameterBuilder<()>,便会推断出 API 包装器的 Parameter 泛型参数就是 ()(即 Void)。
  • 同理,当声明 tupleParamAPI 的类型为 APIParameterBuilder<(id: Int, name: String?)>? 时,编译器就确切地知道,这个 API 的参数类型是一个元组 (id: Int, name: String?)

这种设计带来了显著的优势:

  1. API 合约化:API 的参数类型(它的“状态”)被固化在了定义中,成为一个不可改变的“合约”。
  2. 编译期安全:当你调用 API 时,编译器会强制执行这份合约。
    • 对于 $noParamAPI,调用 request() 方法时如果试图传入参数,编译器会直接报错。
    • 对于 $tupleParamAPI,调用 request(with:) 方法时,你必须传入一个 (id: Int, name: String?) 类型的元组,参数名、类型、数量、可选性稍有差池,都无法通过编译。

这与传统 Moya 中使用 [String: Any] 作为参数的方式形成了鲜明对比。字典是类型不安全的,开发者很容易在运行时因为拼错 key、传错 value 类型而导致请求失败。而 RaAPIWrapper 将这种潜在的运行时错误提前到了编译期,这正是该设计的关键优势之一。

最后,APIParameterBuilder<Input> 扮演了类型转换的角色。它接收强类型的 Input(即 Parameter),并通过一个闭包,将其转换为网络层所需的、经过类型擦除的 any APIParameter 格式。这使得 API 的定义可以保持类型纯净,而将参数构建的细节封装在了构建器内部。


回顾与展望

设计的得与失

通过组合运用 @propertyWrapper 的声明式语法和以泛型为核心的类型系统,RaAPIWrapper 成功地将 API 定义从分散、易错的字符串和字典,转变成了集中的、类型安全的 API “地址簿”。这个以提升代码内聚性和可维护性为目标的尝试,最终收获了编译期安全这一关键优势。

不过,正如一些批判性声音指出的,任何一个设计良好的 Swift 强类型 API,都在某种程度上利用了“类型即状态”。RaAPIWrapper 的创新更多在于其语法和模式,而非思想本身。并且这种“事后诸葛亮”的回顾也让我意识到在技术方面我还差的很远,还有很长的路要走。

清晰的定位:API 定义层

更重要的是,必须明确 RaAPIWrapper 的定位是一个 API 定义的包装层(Wrapper),而非一个大而全的网络请求框架

这一定位意味着它刻意地保持了职责的单一。它只做一件事:以类型安全和高内聚的方式,构建出一个完整的请求元数据。至于这个元数据如何被执行、如何处理回调、如何实现插件、如何管理通用请求头等问题,则完全交由其背后的执行层来处理。

这种关注点分离的设计,恰好回答了“为何不直接用 Moya”的问题:

  • 您可以继续使用 Moya:Moya 就是一个非常出色的执行层。开发者完全可以将 RaAPIWrapper 作为 API “合约”层,将生成的请求数据传递给 Moya Provider,继续享受 Moya 强大的插件系统和请求管理能力。
  • 拥抱变化:如果未来团队希望从 Moya 迁移到 URLSession,或其它新的网络库,您只需要更换执行层的实现,而所有用 RaAPIWrapper 定义的 API 代码都无需改动

未来规划

目前,RaAPIWrapper 尚不自带执行层,也未集成插件系统。未来的规划是提供一个基于 Alamofire 的官方默认执行层,并逐步完善其功能,使其成为一个更加开箱即用的网络解决方案。