TCAでもUseCase/Repository/DataSourceが使いたい

# はじめに

この内容は『iOSアプリ開発のためのFunctional Architecture情報共有会4』のための資料です。

https://connpass.com/event/207051/

---

## 最初に結論

私自身はpointfree自体のやり方を真似していくのが良いと思います。ただ、色々と試してみたという感じです。

結論は

- 何がベストなのかはチームにあってるやり方を採用したらいい
- pointfree自体のやり方でClientをつくるってのもそれがチームに合っていればそれでいい


---

# 導入

[The Composable Architecture (TCA)](https://github.com/pointfreeco/swift-composable-architecture)では副作用実行にグローバルな関数[^1]やClient、またはManagerというのが使われる。公式サンプルだけでなくpointfreeの[ゲームアプリ](https://github.com/pointfreeco/isowords)のコードでもClientがよく出てくる。これを普段よく使われるUseCase/Repository/DataSourceを使いたいというのが発表の主旨。

![名称未設定.004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/305bae86-4150-e3eb-787c-af1e7a0ad2a0.png)


[^1]: もちろんグローバルである必要はなくサンプルだからグローバルな関数になってるだけだとは思う。

---

# TCAの副作用実行のための関数やClientについてのおさらい

---

## （グローバルな）関数？

サンプルではグローバルな関数を使っていた。

次のコードはWeb API呼び出しで任意の数字からその数字にまつわるトリビアを返す非同期な処理。

```swift
func liveNumberFact(for n: Int) -> Effect<String, NumbersApiError> {
  return URLSession.shared.dataTaskPublisher(for: URL(string: "http://numbersapi.com/\(n)/trivia")!)
    .map { data, _ in String(decoding: data, as: UTF8.self) }
    .catch { _ in
      // Sometimes numbersapi.com can be flakey, so if it ever fails we will just
      // default to a mock response.
      Just("\(n) is a good number Brent")
        .delay(for: 1, scheduler: DispatchQueue.main)
    }
    .setFailureType(to: NumbersApiError.self)
    .eraseToEffect()
}
```

---

関数利用側でEnvironmentによってDIする場合。Swiftにおいても、関数も変数として扱えるのでDIするときは`(Parameter) -> Effect`クロージャさえ合えばいい。

```swift
struct EffectsBasicsEnvironment {
  var numberFact: (Int) -> Effect<String, NumbersApiError>
}
```

これで結果を置き換えられるのでXcodeプレビューでも楽だし、テストコードも楽に書ける。

ただ、グローバルな関数として用意するのは複数人でアプリを作るときはあまりやりたくない。サンプルだから良いけども。

---

## Client/Managerとは？

公式のサンプルではClient/Managerは次の通り。

- Client系
    - WebSocketClient
    - SpeechClient
    - DownloadClient
    - AudioPlayerClient
    - WeatherClient
    - AudioRecorderClient
- Manager系
    - LocationManager（これはサンプルというよりTCA公式のLocationラッパーかも）
    - MotionManager（これはサンプルというよりTCA公式のMotionラッパーかも）

ちなみにClientってどんなものなんだろう？

---

Clientには`var`クロージャが処理を返しロジックが入れ替えられるようになってる。

```swift
struct WeatherClient {
  var searchLocation: (String) -> Effect<[Location], Failure>
  var weather: (Int) -> Effect<LocationWeather, Failure>

  struct Failure: Error, Equatable {}
}
```

```swift
struct SearchEnvironment {
  var weatherClient: WeatherClient
  var mainQueue: AnySchedulerOf<DispatchQueue>
}
```

クロージャを変数で入れる場合にEnvironmentに直接入れるのではなく、型としてまとめている。

---

## プロダクション用の実処理自体はextensionに`.live`定数で用意

```swift
extension WeatherClient {
  static let live = WeatherClient( // 初期化してクロージャに処理を用意してそれを定数 .live として返す
    searchLocation: { query in
      var components = URLComponents(string: "https://www.metaweather.com/api/location/search")!
      components.queryItems = [URLQueryItem(name: "query", value: query)]
      
      return URLSession.shared.dataTaskPublisher(for: components.url!) // URLSessionをDIしてない（する気がない
        .map { data, _ in data }
        .decode(type: [Location].self, decoder: jsonDecoder)
        .mapError { _ in Failure() }
        .eraseToEffect()
    },
    ...
```

---

### 短所

- 処理がstaticな定数だから変数を参照することはできない
    - URLSessionをDIすることができない
        - WeatherClient自体を置き換えやすいが、WeatherClient自体をテストする気がない
            - 例えばDBで特定の要素が上書きされるロジックのテストとかはやる気がない

おそらくReducerの仕様さえテストできてれば良くて、そこから奥にあるミスっていうのはReducerの仕様に関するテストコードで気付こうという感じなのではないか。

---

#### 短所を解決するには？

呼び出し時に引数で置き換えたいものを渡す

```swift
  // Reducer内
  case let .locationTapped(location):
    struct SearchWeatherId: Hashable {}

    state.locationWeatherRequestInFlight = location

    return environment.weatherClient
      .weather(location.id, /* ここでURLSessionを渡す */)

```

もしくは定数`.live`のなかで`Factory.getURLSession()`みたいなのを用意してプリプロセッサ的なマクロで切り替えて取り出してもいい（が、その仕組みを自動化せず作るのは自分はあんまり...）。

---

## ここまでのまとめ

- pointfreeの人たちは細かな副作用の単体テストをReducerのテストでカバーしてる
    - 副作用での細かな処理のテストをやるには引数で渡すとか、Factory的なのを内部で切り替える

---

## おまけ: 最近公開されたpointfreeのゲームのコードもClient

https://github.com/pointfreeco/isowords

- DictionaryClient
- FeedbackGeneratorClient
- FileClient
- LocalDatabaseClient
- LowPowerModeClient
- RemoteNotificationsClient
- ServerConfigClient
- UIApplicationClient
- UserDefaultsClient

Managerはない。

---

# UseCase/Repository/DataSourceで副作用を呼び出す

Google I/O 2019 - 2021 Android Appから

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/f68bfece-bdd0-c16e-fdca-6496902fdf30.png)

---

## 層の解説

- ドメイン層（いわくlightweight domain layer）
    - データレイヤーとプレゼンテーションレイヤーの間に位置
        - UIスレッドから離れた場所でビジネスロジックの個別部分を処理
    - UseCaseクラスを中心に構成されてる
    - コールバック地獄を避けるために、LiveData を使用してユースケースの結果を公開
        - 2020あたりではコルーチンも使われはじめる
        - 2018あたりまではexecuteNowメソッドで同期的なものは直接返したり
            - そもそもAndroid Blue Printという設計のお手本リポジトリがあった
- データ層
    - リポジトリモジュールは、すべてのデータ操作を処理し、アプリの他の部分からデータソースを抽象化する役割を担う

データ層についての引用

> 私たちはFirestoreを気に入って使用していましたが、将来的に別のデータソースに交換したくなった場合、このアーキテクチャによってすっきりとした方法で交換することができます）


---

### Google I/O 2019 - 2021 Android AppにおけるUseCase実装

https://github.com/google/iosched/blob/b428d2be4bb96bd423e47cb709c906ce5d02150f/shared/src/main/java/com/google/samples/apps/iosched/shared/domain/sessions/LoadUserSessionOneShotUseCase.kt


```kotlin
...

/**
 * A [UseCase] that returns the [UserSession]s for a user.
 */
class LoadUserSessionOneShotUseCase @Inject constructor(
    private val userEventRepository: DefaultSessionAndUserEventRepository,
    @IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase<Pair<String, SessionId>, UserSession>(dispatcher) {
    // suspendさせてコルーチンなメソッドだが
    override suspend fun execute(parameters: Pair<String, SessionId>): UserSession {
        val (userId, eventId) = parameters
        // 実処理は非同期ではなく同期処理
        return userEventRepository.getUserSession(userId, eventId)
    }
}
```

- セッション情報を取得するUseCase
    - コルーチンで結果`UserSession`を返してる
    - 処理は`userEventRepository`の`getUserSession`メソッド
        - 同期も非同期もインタフェース揃えるため`suspend fun`でコルーチンだけど、同期的に処理[^2]

[^2]: 他にもインタフェースはあり、invokeとかあり、むかしはexecuteNow, performなどがあった。

---


# SwiftでUseCaseを作る場合を考える

- 同期と非同期二つにUseCaseを分ける（まだコルーチンないし）
- Swiftではassociated typeを持つprotocolを作る
    - これを解決する型消去のやり方は複数ある
        - https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0
        
---

## 同期処理するSyncUseCaseの例

```swift
public protocol SyncUseCase where Failure: Error {
    associatedtype Parameters
    associatedtype Success
    associatedtype Failure

    func perform(_ parameters: Parameters) -> Result<Success, Failure>
}
```

このSyncUseCaseを直接利用する型でそのまま準拠してもそのまま使うことはできないので[型消去するがその話は別に書いてる](https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0)。


---

## ProfileをSaveするUseCaseを例に

```swift

public class SaveProfileUseCase: SyncUseCase {
    public typealias Parameters = Profile
    public typealias Success = ()
    public typealias Failure = Error

    private let dataSource: ProfileLocalDataSource

    init(dataSource: ProfileLocalDataSource) {
        self.dataSource = dataSource // initでdataSourceを入れ替えられるように
    }

    public func perform(_ parameters: Parameters) -> Result<Success, Failure> {
        dataSource.save(Profiles: parameters) // 実処理はdataSourceのCore Dataとかでやってる
    }
}

import ComposableArchitecture

extension SaveProfileListUseCase { 
    static func executeEffect(Profile: Parameters) -> Effect<Success, Failure> {
        .result { perform(profile) } // Effectにして結果を返す
    }
}
```

---

### UseCase型の長所と短所

- 長所
    - initでDataSourceを入れるので実行時に引数で用意するわけじゃない
        - 従来のプログラミングスタイルと同じ感じでできる
    - UseCase自体のテストコードを書ける（依存してるDataSourceを置き換えられるし）
- 短所
    - 関数型のやり方に従来のオブジェクト指向型のプログラミングスタイルが混ざることの心理的抵抗感
    - UseCase型がめちゃくちゃ増える

---

# UseCaseは使わないがRepository/DataSourceを使う 

---

UseCase型を使わずに、しかしReducer以外の副作用の単体テストしたいのでRepository/DataSourceは使う場合を考える。

https://qiita.com/yimajo/items/aca14b47dec33f18617c

```swift
// プロフィールを変更して上書きする的なState, Action Environmentの集まり
enum ProfileSettingCore {
    enum State { ... }
    
    struct Action { ... }
    
    struct Environment {
        // DIできるようにする
        var save: (Profile, ProfileDataSource) -> Result<(), Error> 
    }
    
    static let reducer = ...
}
```

---

## ProfileをSaveする例

```swift
// プロフィールを変更して上書きする的なState, Action Environmentの集まり
enum ProfileSettingCore {
    enum State { ... }    
    struct Action { ... }
    
    struct Environment {
        // DIできるようにする
        var save: (Profile, ProfileDataSource) -> Result<(), Error> 
    }
    
    static let reducer = ...
}

extension ProfileSettingCore {
    enum SideEffect {
        static func save(profile: Profile, dataSource: ProfileDataSource) -> Result<(), Error> {
            dataSource.update(profile) // 実質的なUseCaseの処理
        }
    }
}
```

---

### UseCaseを使わないSideEffectでグルーピングされたstatic関数の長所と短所

- 長所
    - シンプル（UseCase型の面倒な型消去なんて考えなくていい）
    - SideEffect自体のテストコードを書ける（依存してるDataSourceを置き換えられるし）
- 短所
    - 副作用実行の引数で依存するRepository/DataSourceを入れるのはかったるい

---

# まとめ

- 何がベストなのかはチームにあってるやり方を採用したらいい
- pointfree自体のやり方でClientをつくるってのもそれがチームに合っていればそれでいい