feather の Framework 分割戦略

概要

feather は2017年にコードベースのフレームワーク化を検討し、swift 化とともに地道に対応を進めています。最近では Framework 分割を行うプロジェクトも珍しくなくなってきたので、今一度どのようにこれを行ったのかまとめることにしました。

アーキテクチャ

開発当初の方針から変わらず、昔ながらの MVC を採用しています。 例えばモデルとして UserAccountManager があり、UserAccount の永続化やロードを行います。それら Manager クラスはシングルトンの ServiceLocator が保持しています。 View Controller は ServiceLocator から取得した Manager クラスを利用します。Manager からは NotificationCenter で通知されます。

このような至って普通の MVC なのですが、API Client を直接利用する画面もあり、あまり頑張ってレイヤーを分けてはいません。

最近コベリンでは RxSwift を使った MVVM 的な構成を良く使い、新しい画面で利用されている部分もあります。feather のコードはかなりこなれていてバグも少ないため、既存のコードを積極的に書き換えることはしていません。

構成

当時いちはやくモダンな構成を実施していた Kickstarter for iOSFirefox for iOS の構成を参考にしました。 Kickstarter for iOS からは、Foundation を拡張するようなコードを Prelude として分けることや、アプリのロジックを Library という Framework に入れたり、API ひとつの Framework に分けることを真似しています。Firefox for iOS は相当細かく Framework を分割しており、ここまで細かくすると大変すぎると思ったのであまり真似していませんが、定数を Shared に入れることを真似しています。

feather では、サービスクラスなどの macOS への移植性やコード品質を高めるため、またできるだけ依存の少ないクラスにするという目的で、次のような構成にしました。

feather
UI などその他全ての実装
TwitterAPI
Twitter の API 仕様を反映した API クライアントを提供するフレームワーク
TwitterService
Twitter クライアントを構成するのに必要なクラスを集めたフレームワーク。TwitterAPI に依存
Prelude
アプリ仕様に無関係なユーティリティを集めたフレームワーク。Foundation にのみ依存
PreludeUIKit
UIKit に依存する Extension を提供
Shared
定数関係
Library
Twitter クライアントを構成するのに必要なクラスで、UIKit, TwitterAPI に依存しないものを集めたフレームワーク

f:id:ryoheyc:20210701184637p:plain

戦略

デザインパターンやアーキテクチャに基づいた分け方ではなく、大まかに TwitterAPI に依存するかどうか・UIKit に依存するかどうかという分け方にしています。

よかったこと

このシンプルなルールのおかげで、無理なくフレームワーク化を実施できました。おかしな依存があるコードはコンパイルできなくなり、コードを綺麗に保てるようになりました。

改めて統一的なアーキテクチャを設計して Framework もこれに従って分割するという方針も考えられますが、feather はすでに巨大なコードベースを抱えていたため、これは困難でした。また、この方針が実施されているかをコンパイル時に分かるようにするのは難しいので、ファイルがフレームワークに移動しただけで依存関係は綺麗にならないという事態になっていたと思います。

いまだに feather に残ったコードは多いですが、ここまでは綺麗、ここからはまだ汚いという区別ができてよかったです。

API だけ分けるのおすすめ

API クライアントとレスポンスのオブジェクト定義を一つの Framework にする分割方法は非常に良かったです。何をどのフレームワークに入れるかという判断は難しいですが、API 関係だけとするのは分かりやすく、NSURLSession とレスポンスの NSDictionary を隠蔽するだけの層としてうまく成り立っています。この隠蔽層は、アプリのアーキテクチャによらず必要になるため、今後も安定して利用できそうです。

その他

feather の Manager 系のクラスでは、データの読み書きを抽象化したクラス Storage を経由してファイルへの読み書きを行っていますが、これを使っていないコードも多くありました。ファイル名のような定数が別のフレームワークに分かれたことで、フレームワーク分割作業のときにこれに依存しないコードにするため、Storage の利用を促進できました。また、テスト時には Storage を差し替えることでインメモリで動作させ、ゴミの残らないテストコードにすることができました。

難しかったこと

  • 依存関係が複雑で切り出せないクラスがかなりある
    • コア部分を全て UIKit に依存しない Framework にして macOS 対応という夢をみた時期もありました・・・
  • ObjC は Framework を分けても #import で別の Framework のコードが読めてしまうので、おかしな依存関係を解消しきれていない
  • 一時はアプリ本体のコードを App という名前の Framework にして、無料版 feather と有料版 feather それぞれのターゲットにはほとんど実装を入れないような構成にしていましたが、Firebase が Static Framework のためビルドできなくなる問題がありやめました
  • Cocoapods で入れているモジュールじゃない Header only のライブラリが Framework 内で使えなかった

XcodeGen

Framework の追加や、ソースコードの Framework への移動はプロジェクトファイルを大きく変更しますが、XcodeGen が導入されていると楽に作業することができます。また、Framework 同士の依存関係も Spec に記述されるため分かりやすくなります。

今後

まだまだ多くの Manager クラスは feather 本体にあり、また View Controller 内に書かれたロジックも多く残っています。それらのクラスは依存が多いために Framework へ切り出しが難しかったので、今後のリファクタリングや機能追加も難しい状態となっていそうです。 feather はユーザー数も多く、リファクタリングによる無用なバグの発生は避けたいので、今後も慎重に整理を行っていきたいと思います。