---
# System prepended metadata

title: Dependency Injection

---

# Dependency Injection

## 🎬 Point・Free
* [video](https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy)
* [source code](https://github.com/pointfreeco/episode-code-samples/blob/main/0016-dependency-injection/Environment.playground/Contents.swift)

## GitHub
<img width="300" src="https://i.imgur.com/kECfNXs.png" style="display: block;margin-left: auto;margin-right: auto;width: 50%;" />

1. 串接 GitHub API
2. TableView 呈現結果
3. 右側顯示發佈時間與現在的時間差

[🔗 Demo link](https://github.com/bing-Guo/DependencyInjectionDemo)

## DI with Protocol
```swift=
protocol GitHubProtocol {
    func fetchRepos(onComplete completionHandler: (@escaping (Result<[GitHub.Repo], Error>) -> Void))
}

// Live GitHub
struct GitHub: GitHubProtocol {
    struct Repo: Decodable {
        var archived: Bool
        var description: String?
        var htmlUrl: URL
        var name: String
        var pushedAt: Date?
    }
    
    func fetchRepos(onComplete completionHandler: (@escaping (Result<[GitHub.Repo], Error>) -> Void)) {
        dataTask("orgs/pointfreeco/repos", completionHandler: completionHandler)
    }
}
```
```swift=
// ViewModel
class ReposViewModel {
    // Inject dependency
    let date: () -> Date // Date.init
    let gitHub: GitHubProtocol
    
    init(date: @escaping () -> Date = Date.init, gitHub: GitHubProtocol = GitHub()) {
        self.date = date
        self.gitHub = gitHub
    }
}
```
```swift=
// Mock GitHub
struct GitHubMock: GitHubProtocol {
    let result: Result<[GitHub.Repo], Error>
    
    // happy path: default return .success case
    init(result: Result<[GitHub.Repo], Error> = .success([
        GitHub.Repo(
            archived: false,
            description: "Blob's blog",
            htmlUrl: URL(string: "https://www.pointfree.co")!,
            name: "Bloblog",
            pushedAt: Date(timeIntervalSinceReferenceDate: 547152021)
        )
    ])) {
        self.result = result
    }
    
    func fetchRepos(onComplete completionHandler: @escaping (Result<[GitHub.Repo], Error>) -> Void) {
        completionHandler(result)
    }
}
```
使用 protocol 做依賴注入會需要許多樣板，每加入新的方法就需要:
1. 新增新方法到 GitHubProtocol
2. 新增並實作方法到 live GitHub
3. 新增 result 到 mock
4. 更新 mock 的 init 並給定預設值（happy path）

## Without Protocol

文中提供另一種方法解決依賴注入，不需要 protocol
```swift=
// App code
struct Environment {
    var date: () -> Date = Date.init
    var gitHub = GitHub()
}

var Current = Environment()
```
1. Environment: 結合所有的 dependencies
2. Current: 表示目前的環境，如在 App code 下，稱作為 live
---
```swift=
struct GitHub {
    struct Repo: Decodable {
        var archived: Bool
        var description: String?
        var htmlUrl: URL
        var name: String
        var pushedAt: Date?
    }
    
    var fetchRepos = GitHub.fetchRepos(onComplete:)
    
    // Use static
    private static func fetchRepos(onComplete completionHandler: (@escaping (Result<[GitHub.Repo], Error>) -> Void)) {
        GitHub.dataTask("orgs/pointfreeco/repos", completionHandler: completionHandler)
    }
}
```
1. 移除 protocol
2. 取而代之是開放所需要對外的 properties，並將 `GitHub.fetchRepos(onComplete:)` 設為在 live 的預設執行內容
3. 把 fetchRepos() 設為 private，這是 live 的實作內容，不需要開放
4. 把 fetchRepos() 設為 static，文中沒特別說明，但如果不這樣做的話，型態會不正確，會被認定為 GitHub instance 下的 func，後續 mock 時就無法覆寫該 properties。（我理解是這樣，有誤麻煩補充 🧐 ）
    ```swift=
    // Static:                 (@escaping ((Result<[GitHub.Repo], Error>) -> Void)) -> ()
    // non-Static: (GitHub) -> (@escaping ((Result<[GitHub.Repo], Error>) -> Void)) -> ()
    var fetchRepos = GitHub.fetchRepos(onComplete:)
    ```
---
```swift=
// ViewModel
class ReposViewModel {
    // Don't need to inject properties
    func loadRepos() {
        // Using Current singleton
        Current.gitHub.fetchRepos { [weak self] result in   
            // do something...
        } 
    }
```
1. 不需要在 init 注入
2. 相依的部分換成 Current
---
```swift=
// Test code
extension Environment {
    static let mock = Environment(
        date: { Date(timeIntervalSinceReferenceDate: 547152051) },
        gitHub: .mock
    )
}

extension GitHub {
    static let mock = GitHub(
        fetchRepos: { callback in
            callback(.success([
                GitHub.Repo(
                    archived: false,
                    description: "Blob's blog",
                    htmlUrl: URL(string: "https://www.pointfree.co")!,
                    name: "Bloblog",
                    pushedAt: Date(timeIntervalSinceReferenceDate: 547152021)
                )
            ]))
        }
    )
}
```
1. 利用 extension 建立 mock，並給予預設值 (happy path)
---
```swift=
class DependencyInjectionDemoTests: XCTestCase {
    var viewModel: ReposViewModel!
    
    override func setUpWithError() throws {
        super.setUp()
        Current = .mock
        viewModel = ReposViewModel()
    }
```
1. 在 test code 當中切換 Current
---
```swift=
func testFailureCase() throws {
    // given
    let expectation = expectation(description: "callback happend")
    Current.gitHub.fetchRepos = { callback in
        callback(.failure(
            NSError(
                domain: "co.pointfree",
                code: -1,
                userInfo: [NSLocalizedDescriptionKey: "Ooops!"]
            )
        ))
    }
    
    viewModel.errorOccurredClosure = { _ in
        expectation.fulfill()
    }
    
    // when
    viewModel.loadRepos()
    
    // then
    wait(for: [expectation], timeout: 1)
}
```
1. 因為 Current 是 mutable，我們可以針對 Current 直接模擬現實中可能的狀況，比如說 failure 狀況

## Q&A
**以下擷取影片中的對話...**
> 教科書上寫說 Singleton 好壞壞，這裡怎麼創造一個 mega-singleton？還用奇怪的大寫命名？

Singletons 通常不太好測試，導致我們用更複雜的依賴注入方法去解決它，比如說在初始化時注入 protocol type。

這裡提供的解法是用 singleton 去解決 singleton 的問題，它有一系列的 mutable properties，是一個我們可以控制的 singleton，可以依照實際情況去替換內容。

要注意的是，目前為止所提到的 Environment 在 production code 都不應該去修改它，這種把所有 dependencies 放在一起的做法是為了方便測試，並且工作量更小。

大寫的部分，影片中沒特別解釋，個人認為是強調它是 global & mutable，比較像是提醒使用者。

## Conclusion
當初想導入這個寫法，是實際遇到一些狀況
1. 想寫 unit test，但有 dependency (API object)
3. API object 是 objective-C 撰寫且歷史久遠，用上述 protocol 解決的話會遇到許多 swift <-> objective-C 型態不支援的問題
4. 撰寫 protocol 會動到 legacy code，洞可能越挖越大

使用本篇的方法體驗不錯，不用動到原本的 legacy code，也不用建立 protocol，核心想法應該是把 function 當作變數丟來丟去（這似乎在 FP 是個很常見的做法！？），需要的時候就把 function 實作內容更換掉。

## Feedback
1. 測試可能會有平行運行問題，所以覆蓋寫可能要在 setUpWithError, finish 處理
2. 也許可以用 compiler directives 去把 var 變成 let