こんにちは。今日は feather for Mastodon のアーキテクチャを紹介したいと思います。
概観
大枠としてはGUIアーキテクチャはMVVMを使用していて、システムアーキテクチャとしてはクリーンアーキテクチャっぽいレイヤードアーキテクチャを採用しています。
またマルチモジュール形式にしていて、Xcodeでみるとこんな感じになっています。
各モジュールの説明
AppLogger
アプリ全体で使うロガーです。このロガーはprotocolが定義してあり、実際にログをどう流すか(print・Clashlyticsにするのかなど)はアプリケーション側で実装を行います。
Mastodon*
マストドンのEntityやAPIClientなどです。もともとは MastodonKit というライブラリを使っていたのですが、メンテされていないことや少し構成を変えたいと思ったため自前でカスタマイズしたものを入れています。
3つのパッケージにわけれていますが、1つのパッケージ中に3つのモジュールを宣言する形式でも良かったかもしれません。
FoundationExt
これはFoundationに生やした便利なextensionを定義しています。
他のいろいろなモジュールで使用したいため一つのパッケージに切り出しました。
Domain
アプリで使うEntity(struct)やRepositoryのprotocolなどが定義されています。
UseCaseはprotocolにわけず、ここに実装をおいています。理由としてはUseCaseはロジックをずばりそのまま書くもので、抽象化したりMockすることは無いと思っているからです。
Infra
Domainで定義されたprotocolの実装が入っています。
例えばDomainではHogeRepositoryProtocolというものが宣言されており、InfraではHogeRepositoryの実装が行われています。APIで通信したり、ファイルに読み書きするような処理です。
TestUtils
テストで使う便利なメソッドが入っています。
所感
各パッケージの役割が明確で、シンプルな構成なので開発時にファイルを作る場所に悩むことはほぼ無いです。
画面ごとにモジュールを分けるような方法(FeatureModule)での分割は行っていませんが、今のfeatherの規模であればそれほどビルドにストレスを感じることはありません。 クリーンビルドで103秒、通常のビルドで13秒程度で終わります。Previewの反映も早いです。
アプリケーションターゲット
feather のアプリそのものはこんな感じにディレクトリを切っています。
画面ごとにディレクトリを作っていて、ViewやSubview、ViewModelなどの関連するものを入れています。
AppEnvironmentsというものを定義してあり、この中にアプリ内で使うRepositoryなどが入っています。プレビュー用とアプリ用で内部で別れています。
これをアプリケーションのRootでEnvironmentObjectとして渡しています。
どうにかしたかったところ
ViewModelがRepositoryを使えるようにしてしまった
一般的にはViewModelは直接Repositoryを使わずUseCaseを介して使うと思います。
しかしUseCaseが一つのRepositoryの一つのメソッドを呼び出すだけみたいなことがよくあるので、それならRespositoryをそのまま呼び出しちゃおうと思いました。
しかしこれだとViewModelに注入するものがRepositoryとUseCaseのに種類が必要になったりしてしまう箇所もありやや気持ち悪い感じがしました。
テスト用のMockのプロトコルが2つ必要になる
以下の2箇所で同じMockが必要になるためやや冗長です(MockはSourceryで生成しています)。
- DomainにあるUseCaseのテストを書こうとするとRepositoryのMockが必要になる
- アプリケーションターゲットでViewModelのテストをするときも同様にRepositoryのMockが必要になる
これもViewModelがUseCaseだけを参照する(&UseCaseのprotocolを作る)ようにすると解決できるかもしれません。
感想
feather は僕が初めてSwiftUIで作ったアプリです。
アーキテクチャは色々検討して今の形に落ち着きましたがまだ色々改善点はあります。 しかしながらコードベースがある程度大きくなってしまったため、気軽にリアーキテクチャするのも難しい現状です。
でも、現状でも全く破綻はしておらずもっと良くはできると思いますがこのままでも行けそうです。