You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Sourcery为带关联类型的泛型网络协议生成Mock?

带关联类型的NetworkHandling协议Mock自动生成优化方案

核心问题拆解

你遇到的泛型作用域报错,本质是带关联类型的协议(比如NetworkHandling)无法直接生成单态Mock类——因为每个请求类型对应唯一的响应类型,默认模板生成的代码没法兼容多组请求-响应的绑定。下面给你两种可行的优化方案,基于Sourcery模板实现。

方案一:类型擦除式Mock(兼容任意请求类型)

1. 模板核心逻辑

通过一个私有的类型擦除容器,把不同请求-响应类型的处理器统一存储,用ObjectIdentifier做类型标识(和你手动实现的逻辑对齐)。

2. Sourcery模板代码

{% for protocol in types.protocols where name contains "NetworkHandling" %}
public class Mock{{ protocol.name }}: {{ protocol.name }} {
    // 存储所有擦除后的请求处理器
    private var handlers: [AnyRequestHandler] = []
    
    // 实现协议要求的handle方法
    public func handle<T: NetworkRequest>(_ request: T) async throws -> T.Response {
        // 匹配当前请求类型的处理器
        guard let matchedHandler = handlers.first(where: { 
            $0.requestTypeID == ObjectIdentifier(T.self) 
        }) else {
            throw MockError.unhandledRequest(T.self)
        }
        // 强制转换是安全的,因为注册时已经绑定了类型
        return try matchedHandler.handler(request) as! T.Response
    }
    
    // 对外暴露的注册方法,用来设置Mock响应
    public func registerMock<T: NetworkRequest>(for requestType: T.Type, handler: @escaping (T) throws -> T.Response) {
        let erasedHandler = AnyRequestHandler(
            requestTypeID: ObjectIdentifier(requestType),
            handler: { input in try handler(input as! T) }
        )
        // 先移除同类型的旧处理器,避免重复
        handlers.removeAll(where: { $0.requestTypeID == ObjectIdentifier(requestType) })
        handlers.append(erasedHandler)
    }
}

// 私有类型擦除容器
private struct AnyRequestHandler {
    let requestTypeID: ObjectIdentifier
    let handler: (Any) throws -> Any
}

// Mock错误定义
enum MockError: Error {
    case unhandledRequest(Any.Type)
}
{% endfor %}

3. 使用方式

生成代码后,你可以这样设置Mock响应:

let mockNetwork = MockNetworkHandling()
// 注册某个请求的Mock响应
mockNetwork.registerMock(for: UserFetchRequest.self) { request in
    return User(id: 1, name: "Mock User")
}

方案二:类型安全的请求-响应绑定(无强制转换)

如果想彻底避免强制类型转换的风险,可以让Sourcery遍历所有实现NetworkRequest的具体类型,为每个类型生成单独的处理器存储和注册方法。

1. Sourcery模板代码

{% for protocol in types.protocols where name contains "NetworkHandling" %}
public class Mock{{ protocol.name }}: {{ protocol.name }} {
    // 为每个请求类型生成单独的处理器变量
    {% for requestType in types.implementing["NetworkRequest"] %}
    private var {{ requestType.name | lowerFirst }}Handler: (({{ requestType.name }}) throws -> {{ requestType.name }}.Response)?
    {% endfor %}
    
    // 实现协议的handle方法,逐个匹配请求类型
    public func handle<T: NetworkRequest>(_ request: T) async throws -> T.Response {
        {% for requestType in types.implementing["NetworkRequest"] %}
        if let concreteRequest = request as? {{ requestType.name }}, 
           let handler = {{ requestType.name | lowerFirst }}Handler {
            return try handler(concreteRequest) as! T.Response
        }
        {% endfor %}
        throw MockError.unhandledRequest(T.self)
    }
    
    // 为每个请求类型生成类型安全的注册方法
    {% for requestType in types.implementing["NetworkRequest"] %}
    public func register{{ requestType.name }}Handler(handler: @escaping ({{ requestType.name }}) throws -> {{ requestType.name }}.Response) {
        self.{{ requestType.name | lowerFirst }}Handler = handler
    }
    {% endfor %}
}

enum MockError: Error {
    case unhandledRequest(Any.Type)
}
{% endfor %}

2. 优势与局限

  • 优势:完全类型安全,没有强制转换,编译期就能发现类型不匹配的问题
  • 局限:需要提前让Sourcery扫描到所有实现NetworkRequest的类型,新增请求类型后需要重新生成Mock代码

关键注意点

  • 不要在Mock类的顶层声明泛型参数,把泛型约束放在方法层面,避免作用域冲突
  • 两种方案都解决了“单个Mock类兼容多请求类型”的问题,前者灵活,后者更安全,根据你的需求选择
  • 生成代码时确保Sourcery能正确识别NetworkRequest协议和它的实现类

内容的提问来源于stack exchange,提问作者Lucas van Dongen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 17:44:54