軟體設計

Swift DI 函式庫 Factory 總整理:和 Swinject 有什麼不同?

Factory 是能在編譯時抓出相依性註冊遺漏的 Swift DI 函式庫。本篇以 2026 年 7 月的 3.3.1 為基準,整理基本用法、Scope 宣告、測試與預覽注入,以及和 Swinject 的差異。

閱讀 5 分鐘
Swift DI 函式庫 Factory 總整理:和 Swinject 有什麼不同? 封面圖

上一篇整理了 SOLID(物件導向五大設計原則)的最後一個字母 DIP(相依性反轉原則)。理解原則後,自然會接著問:「所以,這些相依性在實際 App 中是由誰、如何組裝的?」

專案規模較小時,透過初始化器直接傳入的手動 DI(Dependency Injection,相依性注入)就足夠了。但當畫面增加到數十個、相依性圖變深後,組裝程式碼本身會開始成為負擔。今天介紹適合在這個階段使用的 Swift DI 函式庫 Factory。本篇以 2026 年 7 月的最新版本 3.3.1 為基準。


手動 DI 能撐到什麼程度?

遵循 DIP 的程式碼,大致是這種樣子。

protocol NetworkProviding {
    func fetch(_ url: URL) async throws -> Data
}

final class OrderViewModel {
    private let network: NetworkProviding

    init(network: NetworkProviding) {
        self.network = network
    }
}

依賴協定,實作則從外部注入。到這裡還不需要函式庫。問題在於負責組裝的那一側。

// 某處的組裝點(Composition Root)
let network = NetworkProvider()
let repository = OrderRepository(network: network)
let analytics = AnalyticsService(network: network)
let viewModel = OrderViewModel(repository: repository, analytics: analytics)

當相依性深入三層、四層時,這類初始化程式碼會在每個畫面重複。中間層多一個相依性,就得修改所有經過它的初始化程式碼。這正是想逃到 Singleton 的時刻,但你應該已經知道 Singleton 如何阻礙測試。DI 容器正是代替你處理組裝問題的工具。

Factory 的不同之處:編譯時安全性

提到 Swift DI 函式庫,Swinject 長期以來幾乎是標準選擇。Swinject 以字串或型別註冊,並透過 resolve() 取出,因此漏掉註冊時仍能通過編譯,直到執行時才爆出 nil。也就是說,必須執行 App 才會發現錯誤。

Factory 顛覆了這一點。由於相依性是以 Container 的計算屬性定義,參照不存在的相依性時就無法編譯。拼字錯誤與註冊遺漏都會在建置階段被擋下。

此外,它是執行程式碼不到 1,000 行的輕量函式庫,完全以純 Swift 運作,不需要編譯時程式碼產生腳本。你不必像 Needle 一樣把工具加入建置管線。

3 分鐘整理 Factory 基本用法

使用 SPM(Swift Package Manager)安裝。套件網址是 https://github.com/hmlongco/Factory,3.x 起的匯入方式是 FactoryKit

相依性是透過在 Container 延伸中加入計算屬性來註冊。

import FactoryKit

extension Container {
    var networkService: Factory<NetworkProviding> {
        self { NetworkProvider() }
    }
    var orderRepository: Factory<OrderRepositoryType> {
        self { OrderRepository(network: self.networkService()) }
    }
}

取出時有三種方式。

// 1. 屬性包裝器注入
final class OrderViewModel {
    @Injected(\.orderRepository) private var repository
}

// 2. 直接呼叫
let repository = Container.shared.orderRepository()

// 3. 建構器注入 — 只把組裝交給容器
extension Container {
    var orderViewModel: Factory<OrderViewModel> {
        self { OrderViewModel(repository: self.orderRepository()) }
    }
}

第 3 種方式值得注意。類別本身維持完全不知道函式庫存在的純粹建構器注入形式,只有組裝程式碼交由容器負責。這正好符合 DIP 篇提到的「實作的組裝是外部的責任」這項原則。

在 SwiftUI 中,可以透過 @InjectedObservable 直接取得 Observable 檢視模型。

struct OrderView: View {
    @InjectedObservable(\.orderViewModel) var viewModel
}
容器像輸送帶一樣將相依性模組組裝並送入 App 畫面的插圖
只要在註冊表宣告,組裝就交給容器處理

Scope:透過宣告管理執行個體生命週期

使用 DI 容器的另一個理由,是管理執行個體的生命週期。Factory 只要在註冊表加上一個修飾子即可。

extension Container {
    var networkService: Factory<NetworkProviding> {
        self { NetworkProvider() }.singleton
    }
    var imageCache: Factory<ImageCaching> {
        self { ImageCache() }.cached
    }
}
  • unique — 預設值。每次要求時都建立新的執行個體。
  • singleton — 整個 App 共用一個執行個體。
  • cached — 在重設快取前都回傳相同的執行個體。
  • shared — 只有在有人持有強參考時維持,沒有人使用後就會釋放。

和直接建立全域 Singleton 物件的差異,在於生命週期不是以散落各處的 static let 管理,而是集中宣告在一個註冊表中。之後想把 singleton 改成 cached 時,只要替換一個修飾子即可。

測試與預覽才是真正發揮價值的地方

導入 DI 最實用的理由,終究是測試。Factory 可以直接覆寫註冊。

import FactoryTesting

@Suite(.container)  // 隔離每個測試的容器
struct OrderViewModelTests {
    @Test func loadsOrders() async {
        Container.shared.orderRepository { MockOrderRepository() }
        let viewModel = Container.shared.orderViewModel()
        await viewModel.load()
        #expect(viewModel.orders.count == 3)
    }
}

依照 Swift Testing,在 FactoryTesting target 加上 .container Trait,就不會讓容器狀態在測試之間混用。在 SwiftUI 預覽中也能用相同方式插入 Mock。

#Preview {
    Container.shared.orderRepository { MockOrderRepository() }
    return OrderView()
}

也有只在特定執行環境中自動替換實作的 Context 修飾子。如果只想在 Debug 建置使用 Stub Analytics,可以這樣做。

container.analytics.onDebug { StubAnalyticsEngine() }

同樣地,也能在註冊表中宣告測試、預覽與模擬器環境各自的覆寫。

將機器插槽中的實際模組替換成 Mock 模組的測試注入插圖
測試時插入 Mock,取代真正的實作

Factory 3.x 的變更

以下整理從 2.x 升級過來時需要注意的主要變更。

  • 匯入名稱變更import Factory 改成了 import FactoryKit。遷移大多就是進行這項替換。
  • 完整支援 Swift 6 Strict Concurrency — 註冊 @MainActor 檢視模型時,2.x 必須在閉包中重複寫入 @MainActor in,3.x 只要在 Factory 宣告處加上註解即可。
  • 僅支援 SPM — 已停止支援 CocoaPods。若是 CocoaPods 專案,必須停留在 Factory 2.5.3,或直接嵌入原始碼。
  • 支援 Swift Testing — 包含上面提到的 .container Trait 在內,正式加入測試隔離支援。
Factory 架構圖:App 程式碼、測試與預覽共用 Container 註冊表
App 程式碼、測試與預覽共用同一個註冊表

總結

DIP 告訴我們「依賴抽象,而不是具體實作」,Factory 則降低了在實際程式碼庫中維持這個方向的成本。編譯時安全性會把註冊遺漏變成建置錯誤,而 Scope 與 Mock 替換只需幾行宣告。

不必為了 Factory 重寫已經使用 Swinject 順利運作的專案;但如果是新專案,我認為可以把 Factory 當作預設選擇。它很輕量、導入負擔小,即使不喜歡,也能保留建構器注入結構,只移除容器。

如果手動 DI 已經撐到組裝程式碼開始拖累你,不妨試試看。


參考資料

延伸閱讀