LoginSignup
5
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-21
1 / 27

はじめに

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


最初に結論

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

結論は

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

導入

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

名称未設定.004.png


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


(グローバルな)関数?

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

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

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クロージャさえ合えばいい。

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クロージャが処理を返しロジックが入れ替えられるようになってる。

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

  struct Failure: Error, Equatable {}
}
struct SearchEnvironment {
  var weatherClient: WeatherClient
  var mainQueue: AnySchedulerOf<DispatchQueue>
}

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


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

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の仕様に関するテストコードで気付こうという感じなのではないか。


短所を解決するには?

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

  // 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

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

Managerはない。


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

Google I/O 2019 - 2021 Android Appから

image.png


層の解説

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

データ層についての引用

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


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

...

/**
 * 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を返してる
    • 処理はuserEventRepositorygetUserSessionメソッド
      • 同期も非同期もインタフェース揃えるためsuspend funでコルーチンだけど、同期的に処理2

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


同期処理するSyncUseCaseの例

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

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

このSyncUseCaseを直接利用する型でそのまま準拠してもそのまま使うことはできないので型消去するがその話は別に書いてる


ProfileをSaveするUseCaseを例に


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は使う場合を考える。

// プロフィールを変更して上書きする的なState, Action Environmentの集まり
enum ProfileSettingCore {
    enum State { ... }

    struct Action { ... }

    struct Environment {
        // DIできるようにする
        var save: (Profile, ProfileDataSource) -> Result<(), Error> 
    }

    static let reducer = ...
}

ProfileをSaveする例

// プロフィールを変更して上書きする的な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をつくるってのもそれがチームに合っていればそれでいい

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

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

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5