依存性逆転の原則を使った場合、エラーハンドリングはどうすればいいのか

# はじめに
依存性逆転の原則で抽象化したモジュールがエラーを起こす可能性がある場合、Swiftではどのようにすればいいのか迷ったので考えてみました。
結論は出ていません。

依存性逆転の原則については、Wikipediaを参考にします。

https://ja.wikipedia.org/wiki/依存性逆転の原則

# 概要

ベースとして、公式にあるVendingMachineとVendingMachineErrorによるコードを利用します。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling/

Wikipediaの依存性逆転パターンの部分、

> 低レベルレイヤーはこれらの抽象クラスやインターフェースを継承して生成される。

という内容に従うため、まずはVendingMachineをprotocol化し、従来書いていたコードを `LocalVendingMachine` に変更します。

```swift
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

protocol VendingMachine {
    func vend(itemNamed name: String) throws
}

class LocalVendingMachine: VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0

    func vend(itemNamed name: String) throws  {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }

        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }

        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }

        coinsDeposited -= item.price

        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem

        print("Dispensing \(name)")
    }
}
```

これで、呼び出し側はLocalVendingMachineの実装に依存せず、VendingMachineのprotocolに依存するようになりました。

```swift
let vendingMachine: VendingMachine // ここにLocalVendingMachineなどを注入

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]

func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}

do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
    print("成功! おいしい。")
} catch VendingMachineError.invalidSelection {
    print("無効な選択です。")
} catch VendingMachineError.outOfStock {
    print("在庫切れです。")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("お金が足りません。あと \(coinsNeeded) コイン投入してください。")
} catch {
    print("予期しないエラー: \(error)。")
}
```

# 疑問
呼び出し側が実装に直接依存しなくなったので、VendingMachineのprotocolに適合したクラスであれば差し替えが可能になりました。
しかし、その分VendingMachineのprotocolに適合したクラスが独自のエラーを出す場合が考えられます。

例えば、ネットワーク経由で接続されるVendingMachineの適合クラス `NetworkVendingMachine` に差し替えることを考えましょう。

```swift
struct NetworkVendingMachine: VendingMachine {
    func vend(itemNamed name: String) throws  {
        throw ネットワークのエラー
    }
}
```

NetworkVendingMachineは当然ネットワークに失敗することもありえ、それはエラーとしてthrowされることになります。そのエラー種別はどうするべきでしょうか？

## VendingMachineErrorに新たに定義
```swift
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
    case netWorkOffline //新たに追加
}
```

```swift
struct NetworkVendingMachine: VendingMachine {
    func vend(itemNamed name: String) throws  {
        throw VendingMachineError.netWorkOffline
    }
}
```


このように、VendingMachineErrorに新たにcaseを追加することで、VendingMachineの呼び出し側はVendingMachineErrorのハンドリングを行うことで処理が可能になります。

しかし新たなVendingMachine適合クラスが増えるたびにVendingMachineErrorのcaseを増やすことになるので、VendingMachineErrorはVendingMachine適合クラスの実装全てのエラーをサポートすることになります。

## VendingMachineErrorのprotocol化

VendingMachineErrorもprotocol化して、起こるエラーをVendingMachineErrorに適合させるパターンです。

```swift
protocol VendingMachineError: Error {}

struct OutOfStockError: VendingMachineError {}
struct InvalidSelectionError: VendingMachineError {}
struct InsufficientFundsError: VendingMachineError {
    let coinsNeeded: Int
}

extension VendingMachineError {
    static var invalidSelection: InvalidSelectionError {
        .init()
    }
    
    static var outOfStock: OutOfStockError {
        .init()
    }
    
    static func insufficientFunds(coinsNeeded: Int) -> InsufficientFundsError {
        .init(coinsNeeded: coinsNeeded)
    }
}
```

```swift
// ネットワークエラーの新設
struct NetworkOfflineError: VendingMachineError {}

extension VendingMachineError {
    static var networkOffline: NetworkOfflineError {
        .init()
    }
}

struct NetworkVendingMachine: VendingMachine {
    func vend(itemNamed name: String) throws  {
        throw VendingMachineError.netWorkOffline
    }
}
```

こうすることで、実装クラスはVendingMachineErrorという抽象エラーに適合したものをthrowすることになります。VendingMachineErrorは実装に依存することはありません。

しかし、呼び出し側のコードは各種具体的なエラー型に依存することになります。
呼び出し側で、独自にハンドリングしたいエラーを指定し、個別対応を行うことになります。

```swift
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
    print("成功! おいしい。")
} catch _ as InvalidSelectionError {
    print("無効な選択です。")
} catch _ as OutOfStockError {
    print("在庫切れです。")
} catch let error as InsufficientFundsError {
    print("お金が足りません。あと \(error.coinsNeeded) コイン投入してください。")
} catch _ as NetworkOfflineError {
    print("インターネットに接続されていません")
} catch {
    print("予期しないエラー: \(error)。")
}
```

### VendingMachineErrorに共通メッセージを用意した場合
エラーのプロトコルに共通的な処理を持たせることで、呼び出し側がprotocolのみに依存した処理を書くことが可能です。
例えばエラーメッセージを持たせるようにして、VendingMachineErrorの実装を見ないことも可能になります。

```swift
protocol VendingMachineError: Error {
    var message: String { get }
}
```

```swift
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
    print("成功! おいしい。")
} catch _ as VendingMachineError {
    print(error.message)
} catch {
    print("予期しないエラー: \(error)。")
}
```

しかし、この場合のエラーメッセージはあくまでVendingMachineが呼び出し側にエラーを起こした理由、LocalizedErrorでいう`failureReason`を伝える内容であり、呼び出し側が最終的に表示したいメッセージとは異なる可能性があります。

例えばVendingMachineがネットワークエラーの時、呼び出し側がユーザーにメッセージを表示する役割を担っている場合、更新ボタンを押すべきなのか、それともPull to refreshをするべきなのかVendingMachineの実装がメッセージの内容を設定するのは不可能です。
LocalizedErrorでいう`recoverySuggestion`の部分は呼び出し側によって異なるので、その場合は呼び出し側が実装のエラーを見てメッセージを設定する選択になるんじゃないかと思います。

# まとめ

以上、大きく分けて2パターンについて考えてみました。
個人的にはVendingMachineErrorをprotocol化する方を使おうと思います。