🤩

The Composable Architectureの良さをあらためて整理する2022

2022/12/01に公開

この記事は、The Composable Architecture Advent Calendar 2022 12/1の記事です。

はじめに

The Composable Architecture(TCA)を仕事で使うようになって2年と半年くらいが過ぎてました。アプリ開発のために採用してきた経験もありますが、ここ2年くらいTCAが各社のいろいろなiOSアプリ開発で採用されることもあり、アドバイザーとして仕事を引き受けるというパターンもありました。

なぜこんなにもTCAは魅力があるのか、もしくはTCAを選ばない基準は何か、ということについて2年と半年過ぎた私が今考えていることを書いてみたいと思っています。

The Composable Architectureとは何か

The Composable ArchitectureとはSwiftを用いたiOS/macOS/tvOS/watchOSアプリ開発用の状態管理フレームワークでありGitHub上で公開されています。

さらに詳しくは私の昔の記事が参考になるかもしれません。

https://qiita.com/yimajo/items/77c204ab091223f9cb14

または手っ取り早くREADMEを見れば確実でしょう。

Point-Free

TCAを公開してくれているのは、関数型プログラミングとSwiftについての動画コンテンツ提供しているPoint-Freeというサービスを提供しているBrandon WilliamsさんとStephen Celisさんたちの2人です。

彼らはもともとKickstarterのiOSアプリ開発者で、かつ関数型プログラミングを用いた実用的なiOSプログラミングを広める方たちとして有名です。具体的にはStephenさんはカンファレンス『Functional Swift Conference 2018』にてKickstarterのViewModelをオブジェクトではなく関数へとリファクタリングする方法を講演していたりします。

https://www.youtube.com/watch?v=uTLG_LgjWGA

Point-Freeの良いところは、Swiftで関数型プログラミングをやっていく方法を基礎から説明してくれることで、さらにTCA他多数のライブラリを公開してくれています。

https://github.com/pointfreeco

関数型プログラミング

TCAで扱っている関数型プログラミングの2テーマについてざっくり書いておきます

  • 代数的データ型
  • 副作用の分離

代数的データ型

Point-Freeが説明してくれる関数型プログラミングの基礎として、代数的データ型というのがあります。これを日本語で解説してくれている@kalupas226氏の下記発表が参考になるはずです。

https://speakerdeck.com/kalupas226/swiftfalsestructenumtodai-shu-xue-part1

代数的データ型の考えがあると、処理の結果や方法自体のパターンを型として整合性を担保できるってことです。つまり静的型付け言語であればコンパイラの段階でロジックの不整合を防げるし、型を見ればどのような結果が期待できて不整合がありえないということがわかるという話でしょう。

副作用の分離

関数型プログラミングの根底の考えとして副作用を分離するというものがあり、コードの入力が同じ場合に常に結果へ再現性をもたせることができます。

日本語でわかりやすい説明としては下記の書籍を読むと良いと思います。

https://book.impress.co.jp/books/1114101091

(むしろこのScalaの本にある内容をSwiftでのiOSアプリ開発に置き換えてるのがPoint-Free、という気もしますが)

他にも、日本語の記事では下記がわかりやすいはずです。

https://okapies.hateblo.jp/entry/2016/12/15/021550

上記記事では副作用自体を細かく分離し「副原因」として隠れた入力を表現しようとしているのが良いと思っています。

関数型は現実の問題をシンプルにする

代数的データ型と副作用の分離は関数型プログラミングが抽象的な問題を扱ってるのではなく、現実の問題をシンプルにするアプローチをとっていることがわかりますよね。代数的データ型では不整合を防ぎ、副作用の分離ではテスタビリティを上げてくれるというわけです。

TCAを使わずとも関数型プログラミングを知っていれば、テスタビリティを高くするコツが分かっているわけで、普段のプログラミングについても全然変わってくるはずです。

TCAは何を解決しているのか

繰り返しになりますが、TCAは関数型プログラミングを根底としています。そのことでアプリの状態変更を副作用を分離したReducer関数でのみ行うことでテスタビリティを上げて、コードの見通しを良くしています。そしてサンプルコードが豊富なことで設計についてチーム内でコンセンサスがとりやすいのも良さだと私は考えます。

解決していることを箇条書きすると

  • テストコードを書きやすくする
  • Reducerにより状態を変更する処理の見通しをよくしている
  • 設計方針についてTCAのやり方はサンプルコードが多いためコンセンサスがとりやすい

TCAによる状態管理とテストコードの書きやすさ

TCAはReducerの関数によるアプリケーションの状態管理を行うフレームワークです(ざっくり言うとアプリの状態をモデリングしています)。メリットとしてアプリの状態が変更される処理の見通しが良くなり、開発者がコードから実装を理解しやすくなっています。

さらにTCAはReducerのテストをするための仕組みを備えていることで、実装された処理が予測通りかを検査することが容易になっています。さらに、DIのための仕組みも用意されており、副作用を呼び出すためのオブジェクトなどを本番用、テスト用、プレビュー用などに入れ替えることが容易です。

他人の書いたTCAのReducer関数でも、何が足りないのかを判断しやすく、最終的には誰が書いても同じようなテストコードになるはずです。

Reducerのテストコードについて別記事として、TCAのテストコードについて解説を書いているので詳しくはそちらを参照してみてください。

そもそもなぜテスタビリティを高く保つことが必要かというと、iOSアプリは本来ユーザの期待を裏切らない使いやすい存在でなければいけないわけですが、リアルタイムなユーザ操作を受け付けるイベントドリブンなアプリケーションの開発というのは単に使いやすいアプリケーションを目指すだけで難しい事が多いからです。さらにiOS(Apple)のSDKは基本的にクローズドなコードであり開発者はフレームワークの内部を読むことができません。モバイルアプリ開発では動作するOSのバージョン違いなども考慮する必要があることがほとんどです。SDKのバグは存在するし、公式リファレンスの間違いも普通にあります。さらにSDKとして提供されるフレームワークの型はテストコードのために決まった出力を返すようにすることも簡単ではないことがあります。

そのため、SDKのコードでもテスト用に置き換え可能にし、自分たちが書いたコードについて挙動を再現可能にするということに意味があるわけです。ユーザにとって良い体験を持ってもらうために、こちら側でテストして本質的な問題が何かということを明確にする、その方法として関数型プログラミングがあるのだと思います。

TCAのやり方はサンプルコードが多いため設計方針についてコンセンサスがとりやすい

TCAはGitHub上のリポジトリ自体に多くサンプルが含まれています。これによって様々な実装パターンを確認することができ、自分たちがやりたいことを見つけることができるはずです。

また、TCAを利用したisowordsという実際にリリースされているアプリのコードも公開されています。

設計のコンセンサスがとりやすいことが何を解決しているのか、ということについては別のやり方と比較しなければいけないので(私は)あまりやりたいことではありませんが、下記はそれについて少しだけ書いておきます。

設計思想が統一されていなかったりすると何が困るのか

私は普段仕事でTCA以外を使ってもいますが、複数年運用されているコードには設計思想が統一されておらず、機能によって副作用を呼び出そうとしているのが~Managerだったり~Storeなど様々なものを目にします。これらはその都度ベストを尽くされて生み出されたものでしょうが、ドキュメント化されずに責任範囲が広すぎるManagerStoreは機能追加や修正によってさらに責任が広がる傾向を感じます。

または、それらが状態を持っていることで、機能の変更や修正による影響範囲を予測する必要性が生じ、不具合を修正したり機能を追加する際に悩ましく感じる部分です。

TCAでもそれらの様々な型を使うことはできますが、主作用はReducerにより行われそれとは別に副作用の結果はEffectTaskとなることがはっきりしているため、副作用を担当する型が状態を持つような設計にすることは例外です(例外といってもManagerを作って状態を持たせる必要が生じることもあります)。

TCAを使わずリアクティブプログラミング(RP)をやっていく上での課題

TCAを使わずにRPを採用することで次のような課題を抱えていると私は考えています。

  • 非同期処理を同期的にも扱えるようになるRPは難しすぎてアプリ開発の敷居を上げすぎている
    • 解決したい問題に対して解決方法自体の扱いが難しい
    • CombineフレームワークはクローズドなコードのためRPの結果が不具合なのか見極めが難しい
      • 例えばオペレータの組み合わせによってストリームがHotに自動的に変換されるように思える

TCAを使わずリアクティブプログラミング(RP)でMVVMをやっていく上での課題

さらにRPを使ってMVVMを採用してしまうと次のような課題を感じるのではないでしょうか

  • MVVMのViewModelを採用すると設計のコンセンサスをメンバー間で取るのが難しい
    • ViewModelを「ViewのModel(Viewを抽象化したもの)」または「ViewがModelを使いやすくするもの」と考えたりと複数の別パターンが生まれやすい
  • MVVMのModelについてもメンバー間でコンセンサスを取る必要がある
    • 結局ViewModelもModelもちゃんとドキュメンテーションできればいいがそれができることは稀

もちろん上記のMVVMの課題をTCAならすべて解決できるとまでは言い切れませんし、TCAも初学者にとってはアプリ開発の敷居を上げています。

さらにうまくやれるチームならコンセンサスをとってサンプルコードを書き、Swift Concurrencyを使って他の方法(たとえばMVP)で解決できる程度の課題です。MVVMについて批判するわけではなく、それぞれのやり方にはそれぞれの課題があり、メンバーによって向き不向きがあるでしょうし、作りたいアプリによっては方針など決めずにいくらでも自由にできるほうが良いことだってあるはずです。

TCAを採用しないというパターンについて

ここまでなぜTCAが解決している問題点を明らかにしたいかというと、それはTCAを使ってアプリを開発することは問題解決の一つの解法でしかないためです。他にいくらでもある方法の一つであり、あなたが抱えている問題を解決するかどうか、ベストな選択かどうかはわかりません。

例えば私に「TCAを採用するか考えてください」と言われた際に考慮することを書き出してみます

  • メンバーがテストコードを書いたことがあるかどうか
  • メンバーが関数型プログラミングを知っているかどうか
  • メンバーがTCA自体のコードを読んでも何をやってるかわかるかどうか
  • チームにTCA自体のコードを読めて問題があっても解決できるか

もったいぶった言い方をせずはっきりと私の意見を書くと、もしもテストコードを書いたことがないならそもそも関数型プログラミングのメリットは感じられないはずです。メリットを感じられない手段を選ぶのはおすすめできません。さらにTCAを使うことでコードをより良く書けるのではなく、TCAを使っても副作用を分離しテスタビリティを高く保つことができなければ、無茶苦茶なコードになるのは容易です。高いテスタビリティを保っているかどうかはテストを書いてはじめて維持ができます。

チームで誰かがTCAのコードを読めるというのも重要で、これができないとフレームワークの不具合なのか仕様なのか判断が付きづらいはずです。

その他良いところ

コミュニケーション

その他TCAの良いところは、GitHub上のDiscussionsSwift ForumsでPoint-freeのお二人がコミュニケーションを活発にやってくれてて質問に答えてくれるところでしょう。そうやってコミュニティが育ってるのでお二人が知らなそうな分野は違う人が答えてくれていたりします。

TCAで伸びしろがあるなあと感じるところ

TCAのわかりづらさもあります。質問されたこと

  • 基本を理解してサンプルコードを読み込んで自分で気がつく必要があること
    • 合成された親子関係があるStateやActionが連携できるがそれに気づきづらい
    • バックグラウンド(ユーザ操作外)でDBの監視をしてActionを動かす方法
  • 経験上で判断すること
    • ViewStateを使うタイミング(Stateを分割してView用としたほうがいいのか)
    • ViewStore自体をObservableObjectにしても使えてしまう(WithViewStoreを使わないパターン)
  • モヤッとすること
    • 外部から動作させたくないActionをどうするか
    • サンプルコードがSwiftのDesign Guidelinesに沿った命名になっていない(ex: APIClientではなくApiCLient)
    • DIされる副作用同士の連携

量が多すぎて個別の記事にしたほうが良いと思うので取り上げませんが、一つだけ「DIされる副作用同士の連携」について書いておきます。

DIされる副作用同士の連携

実例がわかりやすいと思うので、isowordsのAPIClientで接続するWeb APIのbaseURLを決定する部分を見てもらいたいです。

https://github.com/pointfreeco/isowords/blob/a02688813bfe4b8b107f1f3e6e36cfc4a26e561a/Sources/ApiClientLive/Live.swift#L8-L24

DIされるAPIClientのLiveでUserDefaultsを使っています。こういうパターンは自分たちのアプリでもよくあるパターンなんですが、UserDefaultsやKeyChain自体をそれ単体で~Clientで使う場合ももちろんあり、別々の~Clientにそれぞれの実体の詳細を書かないといけない。こうなってくると~Clientを経由するということができていないのでモヤッとはします。副作用のオブジェクトを構造化してしまうとそれはそれで別の課題を抱えることになるというのが(モヤッとするというか)伸びしろが有りますねえ。

Discussion