首页
/ ECC 仓库中的 Swift 设计模式指南:协议导向、值类型、Actor 并发与依赖注入

ECC 仓库中的 Swift 设计模式指南:协议导向、值类型、Actor 并发与依赖注入

2026-09-06 18:59:25作者:仰钰奇

本文围绕开源仓库 GitHub_Trending/ev/ECC 中面向 Swift 开发的 steering 规则文档 .kiro/steering/swift-patterns.md 展开。它是一份面向 AI 编程助手(Claude Code / Codex / Kiro 等)的“方向性规则”,用于约束、引导 Agent 在与 *.swift 文件交互时遵循一套高质量的 Swift 设计惯例。读完本文,你将理解这套惯例在仓库内的完整落点:它不仅存在于 steering 文件本身,还以 rules/swift/patterns.md(规则镜像)、两个可直接复用的技能 swift-actor-persistenceswift-protocol-di-testing,以及 swift-reviewer 评审 Agent 的形式贯穿整个仓库,从而形成“写码有模板、评审有标准、测试有方法”的闭环。

1. 这份文档在仓库中扮演什么角色

在 ECC 仓库中,.kiro/ 目录是面向 Kiro(一个 IDE 端 Agent 运行环境)的一套可安装组件集合,其 README 将其定义为“Bring Everything Claude Code (ECC) workflows to Kiro”。.kiro/steering/ 下共有 22 个 steering 文件,分别面向 dev、review、research 等不同模式以及各语言子集,而 swift-patterns.md 专门约束 Swift 代码的书写方式。

该文件的 front matter 给出了它的生效条件:

inclusion: fileMatch
fileMatchPattern: "*.swift"
description: Swift-specific patterns including protocol-oriented design, value types, actor pattern, and dependency injection

即:凡是进入 Agent 上下文的 *.swift 文件,都会触发该 steering 规则的注入,使 Agent 在生成、修改、评审 Swift 代码时遵循其中定义的四种模式。

同时,这份内容并非孤本。在仓库根目录的 rules/swift/patterns.md 中存在内容一致的规则文件,其 front matter 将生效范围进一步明确为:

paths:
  - "**/*.swift"
  - "**/Package.swift"

并注明“This file extends common/patterns.md with Swift specific content”——也就是说,rules/swift/.kiro/steering/ 是同一套语言策略在不同 Agent 平台(Claude Code 风格 rules 与 Kiro 风格 steering)下的两种分发形态。围绕它展开的,还有 rules/swift/coding-style.md(编码风格)、rules/swift/testing.md(测试规范)、rules/swift/security.md(安全要求)与 rules/swift/hooks.md(编辑后自动检查)。

下文按原文档的四个小节逐层展开,并引入仓库内对应技能与评审 Agent 的实现作为纵深依据。

2. 协议导向设计:小协议 + 协议扩展默认实现

原文档给出的第一条准则是“定义小而聚焦的协议,用协议扩展提供共享默认实现”,并给出了一个典型仓储接口:

protocol Repository: Sendable {
    associatedtype Item: Identifiable & Sendable
    func find(by id: Item.ID) async throws -> Item?
    func save(_ item: Item) async throws
}

注意这个接口的三个细节,它们共同体现了“面向协议设计 + Swift 并发安全”的叠加要求:

  • associatedtype Item: Identifiable & Sendable:仓储所管理的实体需要可被唯一标识(Identifiable),并且能安全地跨越并发隔离域传递(Sendable)。
  • find(by:)save(_:) 都是 async throws 方法:读取与写入被建模为可能失败、可能耗时(涉及 I/O 或网络)的异步操作。
  • 协议自身标记为 Sendable:在 Swift 6 严格并发检查下,跨 actor 传递协议类型时需要满足 Sendable

从仓库中与之配套的协议示例看,这套“小协议”思想在 skills/swift-protocol-di-testing/SKILL.md 中被进一步细化为一套可落地的分工:每个协议只负责一类外部关注点,例如把“文件系统能力”拆成三个独立协议:

// 文件系统访问入口
public protocol FileSystemProviding: Sendable {
    func containerURL(for purpose: Purpose) -> URL?
}

// 文件读写操作
public protocol FileAccessorProviding: Sendable {
    func read(from url: URL) throws -> Data
    func write(_ data: Data, to url: URL) throws
    func fileExists(at url: URL) -> Bool
}

// 书签存储(例如沙盒化 App 的授权恢复)
public protocol BookmarkStorageProviding: Sendable {
    func saveBookmark(_ data: Data, for key: String) throws
    func loadBookmark(for key: String) throws -> Data?
}

该技能文件明确把“不要创建涵盖所有外部访问的巨型协议”列为反模式,与 steering 文档“小而聚焦”的表述一一对应。设计时把握一条铁律:协议应当围绕行为(role)而非类型(type)命名,并尽量保持单一职责,这也会在评审阶段由 swift-reviewer Agent 校验。

3. 值类型优先:struct 承载模型,enum 建模状态机

原文档对值类型的要求分为两条:

  • 用 struct 表示数据传输对象(DTO)与模型
  • 用带关联值的 enum 建模互斥的状态

对应的状态建模示例是:

enum LoadState<T: Sendable>: Sendable {
    case idle
    case loading
    case loaded(T)
    case failed(Error)
}

这个 LoadState 泛型枚举把“加载中视图模型的全部可能状态”收拢到一处:未开始(idle)、加载中(loading)、已成功携带数据(loaded(T))、已失败携带错误(failed(Error))。编译器因此可以穷尽匹配所有分支,杜绝遗漏状态导致 UI 显示不一致的 bug。而 Sendable 约束则保证该状态值可以安全地作为 actor 方法返回值或 @Observable 视图模型属性跨隔离域传递。

ECC 仓库对“值类型”的支持并不停留在写法层面,rules/swift/coding-style.md 进一步给出约束:

  • 默认使用 let 而非 var,只有当编译器要求可变时才改用 var
  • 默认使用带值语义的 struct,只有当确实需要“身份/引用语义”(如类标识、生命周期管理)时才使用 class

将 enum 状态机 + struct 模型结合,就可以在视图层得到可预测、可测试的状态流转:由 loadingloaded(data),或由 loadingfailed(error),UI 对每一种状态都有确定的渲染策略。

4. Actor 模式:用编译器保证代替手工同步

原文档的第三条准则是“用 actor 管理共享可变状态,而不是锁或派发队列”,其最小示例为:

actor Cache<Key: Hashable & Sendable, Value: Sendable> {
    private var storage: [Key: Value] = [:]

    func get(_ key: Key) -> Value? { storage[key] }
    func set(_ key: Key, value: Value) { storage[key] = value }
}

actor Cache 的所有实例方法在 actor 隔离域内串行执行,storage 字典被 private 封装,外部只能通过 await cache.set(...) 访问。从源码结构看,KeyValue 都被约束为 Sendable,这保证了跨隔离域传参是安全的。相比 NSLock/DispatchQueue + class 的做法,actor 方案把“数据竞争”从运行时错误升级为编译期错误——误写并发访问代码会直接无法通过编译。

4.1 纵深:从最小缓存到完整持久化仓储

如果仅停留在最小示例,读者很难评估 actor 在真实工程里的价值。为此,仓库配套技能 skills/swift-actor-persistence/SKILL.md 提供了从“内存缓存 + 文件落盘”到“与视图模型联动”的完整范式。它的核心是一个泛型 actor 仓储:

public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
    private var cache: [String: T] = [:]
    private let fileURL: URL

    public init(directory: URL = .documentsDirectory, filename: String = "data.json") {
        self.fileURL = directory.appendingPathComponent(filename)
        // 同步加载:actor 隔离尚未生效,允许在 init 中直接读文件
        self.cache = Self.loadSynchronously(from: fileURL)
    }

    // MARK: - Public API

    public func save(_ item: T) throws {
        cache[item.id] = item
        try persistToFile()
    }

    public func delete(_ id: String) throws {
        cache[id] = nil
        try persistToFile()
    }

    public func find(by id: String) -> T? {
        cache[id]
    }

    public func loadAll() -> [T] {
        Array(cache.values)
    }

    // MARK: - Private

    private func persistToFile() throws {
        let data = try JSONEncoder().encode(Array(cache.values))
        try data.write(to: fileURL, options: .atomic)
    }

    private static func loadSynchronously(from url: URL) -> [String: T] {
        guard let data = try? Data(contentsOf: url),
              let items = try? JSONDecoder().decode([T].self, from: data) else {
            return [:]
        }
        return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
    }
}

这一实现把 steering 文档的抽象原则落地为工程决策,其“关键设计决策表”可帮助我们理解每个取舍:

决策 理由
用 actor(而非 class + 锁) 编译期强制线程安全,无需手工同步
内存缓存 + 文件持久化 读走缓存快、写落盘可靠
init 中同步加载 规避异步初始化带来的复杂度
以 ID 为键的字典 支持 O(1) 的按标识查找
泛型约束 Codable & Identifiable 任意模型类型均可复用
原子写入(.atomic 崩溃时避免产生半截文件

使用方注意:由于 actor 隔离,所有对外方法调用都必须 await,例如:

let repository = LocalRepository<Question>()

// 读:走内存缓存,O(1)
let question = await repository.find(by: "q-001")
let allQuestions = await repository.loadAll()

// 写:更新缓存并原子落盘
try await repository.save(newQuestion)
try await repository.delete("q-001")

若与 SwiftUI 的 @Observable 视图模型配合,就可以把 actor 仓储注入 ViewModel,实现“视图操作 → actor 落盘 → 刷新列表”的响应式链路:

@Observable
final class QuestionListViewModel {
    private(set) var questions: [Question] = []
    private let repository: LocalRepository<Question>

    init(repository: LocalRepository<Question> = LocalRepository()) {
        self.repository = repository
    }

    func load() async {
        questions = await repository.loadAll()
    }

    func add(_ question: Question) async throws {
        try await repository.save(question)
        questions = await repository.loadAll()
    }
}

4.2 actor 的实用边界与反模式

skills/swift-actor-persistence/SKILL.md 同时给出最佳实践与反模式,可作为 steering 文档的补充判据:

最佳实践:

  • 所有跨越 actor 边界的数据都应是 Sendable 类型;
  • 保持 actor 公共 API 最小——只暴露领域操作,不暴露持久化细节;
  • 文件写入一律 .atomic,防止写入中途崩溃损坏数据;
  • 本地小文件在 init 中同步加载,避免异步初始化带来的复杂度;
  • @Observable ViewModel 结合以获得响应式 UI 更新。

反模式:

  • 在新 Swift 并发代码中继续使用 DispatchQueue/NSLock 代替 actor;
  • 把内部缓存字典直接暴露给外部调用者;
  • 忘记 actor 所有方法调用都是 await
  • nonisolated 绕过 actor 隔离(等于自废武功)。

5. 依赖注入:默认参数注入协议,生产用默认、测试注 Mock

原文档的第四条准则给出了一个精炼的“默认参数注入”模式:

struct UserService {
    private let repository: any UserRepository

    init(repository: any UserRepository = DefaultUserRepository()) {
        self.repository = repository
    }
}

要点有两点:

  1. 面向协议编程:属性类型是协议 any UserRepository,而非具体类型 DefaultUserRepository
  2. 默认参数实现“可选的注入”:生产环境不传参即可获得真实实现;测试环境传入 Mock 即可替换依赖,无需修改 UserService 本身的代码,也无需依赖 #if DEBUG 之类条件编译。

5.1 纵深:一套完整的“协议 + 默认实现 + Mock”装配

原文档只给了结构示例,仓库配套技能 skills/swift-protocol-di-testing/SKILL.md 则给出了完整的四步装配法,适合用于文件系统、网络、iCloud 等外部依赖的抽象:

第一步:定义小而聚焦的协议(见上文 FileSystemProviding / FileAccessorProviding / BookmarkStorageProviding)。

第二步:提供生产默认实现

public struct DefaultFileSystemProvider: FileSystemProviding {
    public init() {}

    public func containerURL(for purpose: Purpose) -> URL? {
        FileManager.default.url(forUbiquityContainerIdentifier: nil)
    }
}

public struct DefaultFileAccessor: FileAccessorProviding {
    public init() {}

    public func read(from url: URL) throws -> Data {
        try Data(contentsOf: url)
    }

    public func write(_ data: Data, to url: URL) throws {
        try data.write(to: url, options: .atomic)
    }

    public func fileExists(at url: URL) -> Bool {
        FileManager.default.fileExists(atPath: url.path)
    }
}

第三步:为测试创建带“可注入错误”的 Mock

public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable {
    public var files: [URL: Data] = [:]
    public var readError: Error?
    public var writeError: Error?

    public init() {}

    public func read(from url: URL) throws -> Data {
        if let error = readError { throw error }
        guard let data = files[url] else {
            throw CocoaError(.fileReadNoSuchFile)
        }
        return data
    }

    public func write(_ data: Data, to url: URL) throws {
        if let error = writeError { throw error }
        files[url] = data
    }

    public func fileExists(at url: URL) -> Bool {
        files[url] != nil
    }
}

Mock 的关键设计是 readError / writeError 两个可配置错误属性——它让“磁盘损坏、文件缺失、写入失败”等难以在真实环境触发的分支可以被确定性测试覆盖。

第四步:生产对象使用默认参数,测试注入 Mock

public actor SyncManager {
    private let fileSystem: FileSystemProviding
    private let fileAccessor: FileAccessorProviding

    public init(
        fileSystem: FileSystemProviding = DefaultFileSystemProvider(),
        fileAccessor: FileAccessorProviding = DefaultFileAccessor()
    ) {
        self.fileSystem = fileSystem
        self.fileAccessor = fileAccessor
    }

    public func sync() async throws {
        guard let containerURL = fileSystem.containerURL(for: .sync) else {
            throw SyncError.containerNotAvailable
        }
        let data = try fileAccessor.read(
            from: containerURL.appendingPathComponent("data.json")
        )
        // Process data...
    }
}

该技能文件总结的反模式同样值得注意:创建覆盖一切外部访问的“巨型协议”、给没有外部依赖的内部类型做 Mock、用 #if DEBUG 替代依赖注入、与 actor 连用时遗漏 Sendable 约束、以及过度设计——若某类型本无外部依赖,就根本不需要为它定义协议。

5.2 与 Swift Testing 结合:DI 的最终目的

DI 的最终目的是让业务逻辑在无 I/O 环境下被确定性测试。仓库 rules/swift/testing.md 明确规定:新测试统一使用 Swift Testingimport Testing),配合 @Test#expect。例如验证创建邮箱非法的用户会被拒绝:

@Test("User creation validates email")
func userCreationValidatesEmail() throws {
    #expect(throws: ValidationError.invalidEmail) {
        try User(email: "not-an-email")
    }
}

skills/swift-protocol-di-testing/SKILL.md 中,可以看到 Mock 注入 + Swift Testing 对错误分支的覆盖样例:

import Testing

@Test("Sync manager handles missing container")
func testMissingContainer() async {
    let mockFileSystem = MockFileSystemProvider(containerURL: nil)
    let manager = SyncManager(fileSystem: mockFileSystem)

    await #expect(throws: SyncError.containerNotAvailable) {
        try await manager.sync()
    }
}

@Test("Sync manager reads data correctly")
func testReadData() async throws {
    let mockFileAccessor = MockFileAccessor()
    mockFileAccessor.files[testURL] = testData

    let manager = SyncManager(fileAccessor: mockFileAccessor)
    let result = try await manager.loadData()

    #expect(result == expectedData)
}

@Test("Sync manager handles read errors gracefully")
func testReadError() async {
    let mockFileAccessor = MockFileAccessor()
    mockFileAccessor.readError = CocoaError(.fileReadCorruptFile)

    let manager = SyncManager(fileAccessor: mockFileAccessor)

    await #expect(throws: SyncError.self) {
        try await manager.sync()
    }
}

同时 rules/swift/testing.md 还要求每个测试获得全新实例(init 中准备、deinit 中清理,测试间不共享可变状态),并支持参数化测试:

@Test("Validates formats", arguments: ["json", "xml", "csv"])
func validatesFormat(format: String) throws {
    let parser = try Parser(format: format)
    #expect(parser.isValid)
}

覆盖率统计可通过命令执行:

swift test --enable-code-coverage

6. 与并发、安全、工具链的配套约束

四种核心模式之外,同一套策略还通过 rules/swift/coding-style.mdrules/swift/security.mdrules/swift/hooks.md 约束 Swift 代码的其余维度,它们共同支撑 steering 文档中“Sendable 值类型 + actor + 面向协议”的三条并发主线:

  • 严格并发:开启 Swift 6 严格并发检查;优先用 Sendable 值类型穿越隔离域、用 actor 管理共享可变状态、用结构化并发(async letTaskGroup)而非无结构 Task {}
  • 类型化错误:Swift 6+ 使用 throws(LoadError) 类型化抛出与模式匹配,而不是在 catch 里做字符串级判断。
  • 格式化与风格:优先 let、默认 struct,命名遵循 Apple API Design Guidelines,常量用 static let;工具上推荐 SwiftFormat 自动格式化、SwiftLint 强制风格,Xcode 16+ 自带的 swift-format 亦可替代。
  • 安全基线:敏感信息(token、密码、密钥)一律存入 Keychain,严禁放 UserDefaults,严禁硬编码在源码里(反编译可轻易提取);默认启用 App Transport Security,不得随意关闭;用户输入与外部数据(API、Deep Link、剪贴板)使用前必须校验。对应示例:
let apiKey = ProcessInfo.processInfo.environment["API_KEY"]
guard let apiKey, !apiKey.isEmpty else {
    fatalError("API_KEY not configured")
}
  • 编辑钩子:在 ~/.claude/settings.json 等平台配置中注册 PostToolUse 钩子,让 Agent 编辑 .swift 文件后自动执行 SwiftFormat、SwiftLint 与 swift build 类型检查,同时把生产代码中的 print() 标记出来(建议改用 os.Logger 或结构化日志)。

7. 在评审工作流中的应用:swift-reviewer Agent

steering 文件的价值最终要落到“有人照章执行、有人照章审查”。仓库为此提供了专职评审 Agent agents/swift-reviewer.md(在 .kiro/agents/ 下也有对应版本),其定位描述与 steering 文档高度一致:专门评审“protocol-oriented design、value semantics、ARC memory management、Swift Concurrency 与惯用模式”,并要求 所有 Swift 项目必须使用

从该 Agent 的定义可以看到一套标准评审流程:

  1. 先执行 swift buildswiftlint lint --quietswift test,任一失败即停止并报告;
  2. git diff HEAD~1 -- '*.swift'(PR 评审时用 git diff main...HEAD -- '*.swift')定位改动范围;
  3. 聚焦修改过的 .swift 文件展开评审。

其“CRITICAL - Safety”清单也呼应了上述安全规则:禁止生产路径上的强制解包(!)、无理由的 try!as!,禁止硬编码密钥,禁止把敏感数据写入 UserDefaults,禁止无理由关闭 ATS,警惕 SQL/命令注入、路径穿越与不安全的反序列化。协议设计、值语义与并发正确性则在后续评审优先级中逐项核验。

可以这样理解整个体系的分工:steering 文件负责在写码前“给定范式”,rules 细化风格、测试、安全与钩子,skills 提供可直接复用的完整实现模板,swift-reviewer Agent 负责在评审时“按图索骥”。四个环节共享同一套 Swift 设计价值观。

8. 小结

.kiro/steering/swift-patterns.md 用极简篇幅给出了四把“钥匙”,而 ECC 仓库用一整条技能/规则/Agent 链条把钥匙锻造成了完整工具:

Steering 规则 核心要求 仓库配套纵深
协议导向设计 小协议 + 关联类型 + 协议扩展默认实现 swift-protocol-di-testing 技能 中的 FileSystem/FileAccessor 协议族
值类型 struct 做 DTO/模型,带关联值 enum 建模状态 LoadState<T> 状态机与 coding-stylelet/struct 偏好
Actor 模式 共享可变状态交给 actor,告别锁与派发队列 swift-actor-persistence 技能LocalRepository 完整实现
依赖注入 默认参数注入协议,测试注入 Mock DefaultFileAccessor + MockFileAccessor + Swift Testing 样例

对开发者而言,最直接的可操作建议是:新写 .swift 代码时,把本文第 2~5 节的四个模板作为起点;涉及持久化时直接参考 skills/swift-actor-persistence/SKILL.md,涉及文件/网络等外部依赖的测试化改造时参考 skills/swift-protocol-di-testing/SKILL.md;提交前用 swift test --enable-code-coverage 校验覆盖,并允许 swift-reviewer Agent 以 agents/swift-reviewer.md 定义的安全与风格清单完成终审。

相关仓库路径速查:

登录后查看全文
热门项目推荐
相关项目推荐