リファクタリング - 通信周りの実装をRestKitから独自の実装に移行する
これはTImeTree Advent Calendar 2023の13日目の記事です。
こんにちはこんにちは。TimeTreeのiOSエンジニアのmasaichiです。
TimeTreeのiOSクライアントでは、Swift化・実装の近代化の一環として通信周りの実装をRestKitを利用した実装から、独自の実装に置き換えるリファクタリングを行っています。
この記事ではリファクタリングをどう分解して進めたのかをまとめ、紹介したいと思います。
TimeTreeiOSクライアントの通信とデータモデル
TimeTreeはカレンダーシェアアプリで、予定の情報などはサーバから読み込みCoreDataに保存しています。
この際のサーバとの通信・レスポンスのCoreDataのEntityへのマッピングから保存までいわゆる「データの同期」の処理をRestKit (https://github.com/RestKit/RestKit) に任せています。
RestKitはObjective-Cで実装されたAPIClientです。特徴としては通信だけでなく、JSONのオブジェクトへのマッピングを行うことができ、さらにはマッピングの対象としてCoreDataを選ぶことができます。
APIのインターフェースがRestfulであり、クライアントのストレージとしてCoreDataを選択していたTimeTreeにはぴったりのライブラリでした。
私見ですが、TimeTreeがリリースされた2015年当時はSwiftもまだ出たばかりでCodableもなく、JSONのマッパーは乱立している状態でした。また通信のようにスレッドが絡む状況でのCoreDataへの保存は正しく行わないとバグを仕込みがちです。それを一気通貫に行えるRestKitは良い選択だったと思います。
RestKitからの脱却
RestKitは通信からデータの保存までを一手に引き受けてくれる素晴らしいライブラリですが、その責任範囲故に複雑でデバッグやカスタマイズが難しいです。
また、TimeTreeもSwift化を順次進めており、新規のコードはSwiftで書かれています(2023年12月現在、大半のコードはSwiftに移植済み)。
RestKitは実装がObjective-Cなため、どうしてもやりとりをするために@objc
で修飾したコードを書くことや、データモデル、通信周りのメソッドのインターフェースなどで変換が必要になったりと、Swiftを活かしづらいケースがあります。より効果的にSwiftの機能を活用していくために、少しずつ依存をなくしていこうとなりました。
どうやって進めいったか
RestKitからの脱却、つまり大規模なリファクタリングですが、既存の機能を壊さないように進めていく必要があります。
私が参加した当時にRestKitを脱却しようというプロジェクトが立ち上がり済みで、いくつかのリクエストはリプレイスが終わっていましたが、予定、カレンダー、ユーザーなどのリクエストは手付かずでそのままでした。
移行後のフレームはできているが、サービスのコアになるところはそのままという状態ですね。コアなところである故に実装も複雑で慎重さが求められるところです。
進めるにあたっては可能な限り1度に1つのリクエストずつ置き換えていく方針で進め、以下のような手順で置き換えを進めていきました。
- 対象のリクエストの影響範囲の理解
- どの画面のどういう操作を契機に呼び出されているのか
- 対象のリクエスト名でプロジェクト全体を検索して呼び出し元のメソッドを見つけ、さらにそれをFind Call Hierarchyや文字列検索でさらに深掘り・・・のように地道に探していきます
- 内部実装の理解
- リクエストの呼び出しに付随して行っていることの有無。継承ツリーの追跡
- 実際に発行されているリクエストのパラメータはどうなっているか
- テストの計画と単体テストの実装
- 何を単体テストで、何を手による動作確認で賄うか
- リクエストからマッピングまでを単体テストで賄う
- 実装
- テスト
実装自体は付随する処理はあれどAPIを叩いて結果をCoreDataに保存するだけであり、仕組みもすでに作られていたのでそれほど難しいものではありませんでした。
それ以外のところに多くの時間を使いました。
特に実装前の1-3が重要で、ここを調べてきっちり計画と単体テストを作っておくことでスムーズに実装が進められます。
図. あるリクエストの影響範囲のメモ
また実装にあたっては古い仕組みを使っているところは新しいものに置き換えたり、リクエストの呼び出しのインターフェースをasync化するなど近代化も合わせて行っています。async化は呼び出し元と実装の双方をシンプルにできたので採用して良かったとことの1つです。
RestKitからの学び
記事の本筋からすると蛇足ですが、リファクタリングを進めるにあたりRestKitの実装を読むことが度々ありました。
「こんなクラス・関数があったのか」と思うことがあったので紹介します。
NSRelationshipDescription
NSRelationshipDescription
はエンティティ間の関連の定義の詳細です。
NSEntityDescription
のrelationshipsByName
に関連の名前を渡すことで取得できます。
以下は予定から参加者一覧の関連の定義を引くコードです。
let eventEntityDescription = Event.entity()
let usersRelationshipDescription = eventEntityDescription.relationshipsByName["users"]
relationshipDescriptionはその関連の先のEntityDescriptionや「対多関連」なのか、対多関連なら順序を保持してるのかどうか、などのメタデータを実行時に参照できます。
これを何に使っているのかというと、レスポンスのマッピング時にリレーションを構築するためです。
マッピングの実行時にapiのレスポンスの要素から関連先のエンティティを引き当て、関連を設定します。その際に「対他関連なのかどうか」を参照し処理を呼び分けています。
// 簡易コード
let objects = entites(from: response)
if relationshipDescription.isToMany {
entity.setValue(objects, forKeyPath: relationshipName)
} else {
entity.setValue(objects.first, forKeyPath: relationshipName)
}
obtainPermanentIDsForObjects
obtainPermanentIDsForObjects
は渡したManagedObjectのObjectIDを永続化IDに変換します。
NSManagedObjectにはobjectID
というプロパティがあります。このidは一意になりますが、新規に作成したobjectの場合ストアに保存されるまで一時的な値であり、保存後に変わってしまいます。
通常あまり意識する必要はないですが、マルチスレッド環境下でCoreDataを利用する場合など、temoraryのidのまま処理を進めるとオブジェクトが分裂するなどよくわからない問題に繋がります。
obtainPermanentIDsForObjects
を呼ぶことで永続化IDを振ることができるので、マルチスレッド環境下でのCoreDataの動作を安定させることができます。
まとめ
現状で残すリクエストはあとわずかというところです。
サービスのコアに関わるリファクタリングですが、手順を踏みテストを厚くすることで、大きなバグを出すことなく進められています。
リファクタリングの進め方の参考になると幸いです。
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion