3-30.【函数式编程】你如何用 Monad / Functor 思想重构异步网络请求链,提高可组合性和可测试性?

9 阅读2分钟

1️⃣ 问题背景

传统异步网络请求链常见写法:

fetchUser { user in
    fetchPosts(user.id) { posts in
        fetchComments(posts) { comments in
            print(comments)
        }
    }
}

问题:

  1. 回调地狱,嵌套多层
  2. 不可组合,无法在不同请求间自由组合
  3. 副作用分散,难以测试

2️⃣ Functor / Monad 思想

  • Functor (map)

    • 对容器(异步值、Publisher、Result 等)中的值应用函数
    • 不改变容器结构
    • 适合对结果做同步转换
  • Monad (flatMap)

    • 对容器值应用返回容器的函数
    • 可以链式组合多个异步操作
    • 自动“平铺嵌套容器”,保持类型安全
    • 遇到失败自动短路(Result/Publisher)

核心思想:把异步请求视作 容器,函数返回值也是容器 → 链式组合


3️⃣ 使用 Result / Combine 重构异步请求

(1) 使用 Result 封装异步操作

import Foundation
import Combine

struct User { let id: Int }
struct Post { let id: Int }
struct Comment { let id: Int }

func fetchUser() -> AnyPublisher<User, Error> {
    Just(User(id: 1))
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
}

func fetchPosts(userId: Int) -> AnyPublisher<[Post], Error> {
    Just([Post(id: 10), Post(id: 11)])
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
}

func fetchComments(posts: [Post]) -> AnyPublisher<[Comment], Error> {
    Just(posts.map { Comment(id: $0.id * 100) })
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
}

每个函数返回 Publisher → 异步容器


(2) 链式组合 (Monad flatMap)

let cancellable = fetchUser()
    .flatMap { user in
        fetchPosts(userId: user.id)       // flatMap 绑定下一步异步操作
    }
    .flatMap { posts in
        fetchComments(posts: posts)
    }
    .sink { completion in
        switch completion {
        case .finished: break
        case .failure(let error): print("Error:", error)
        }
    } receiveValue: { comments in
        print("Fetched comments:", comments)
    }

优势

  1. 链式组合 → 可插入任意异步操作
  2. 类型安全 → 输出类型清晰
  3. 错误传播 → 遇到失败自动短路,进入 sink 的 failure 分支
  4. 副作用集中sink / assign

(3) 同步 Functor map

如果想对数据做同步转换:

let processed = fetchUser()
    .map { user in user.id * 100 }  // Functor map
    .flatMap { id in fetchPosts(userId: id) }
  • map 对 Publisher 内值做同步计算
  • flatMap 对 Publisher 内值做异步操作 → Monad

4️⃣ 测试友好性

  1. 纯函数封装网络请求
func mockFetchUser() -> AnyPublisher<User, Error> {
    Just(User(id: 42))
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
}
  • 纯函数 → 可单元测试
  • 无副作用,外部 sink 可用测试用例捕获结果
  1. 链式组合可拆分测试
let testPosts = fetchPosts(userId: 42)
    .sink { completion in
        ...
    } receiveValue: { posts in
        assert(posts.count == 2)
    }
  • 每一步都可以独立测试
  • 保持高可组合性

5️⃣ 总结设计模式

概念应用到异步请求链
Functor (map)对 Publisher 内部值做同步转换
Monad (flatMap)链式组合异步请求,平铺容器
错误传播Publisher 内错误自动短路,进入 sink 的 failure
副作用控制所有输出 / 打印集中在 sink / assign

💡 核心思想
将异步请求视作 容器 → map / flatMap 链式组合 → 保持纯函数 → 副作用集中 → 可测试、可组合