エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

7年間運用している主力iOSアプリをリファクタリングしている話

こんにちは、マルチデバイスチームでスマホアプリのエンジニアをしております星野です。

エムスリーでは医療従事者向け/一般の方向けに複数のアプリを開発していますが、その中でも特に主力のアプリである m3.com アプリで現在行っているリファクタリングについてお話ししていきます。

※iOS/Android のアプリがあり、両方のリファクタリングを進めていいますが、今回は iOS 版の話です
※この内容は私個人ではなく、チーム全体で行なっていることをまとめたものです

イントロダクション

f:id:tmnet357:20220308094843p:plain
m3.com アプリ

m3.com アプリは医療従事者向けのサービスのため、残念ながら一般の方はログインできませんが、イメージ的には複数のサービスが載っているポータルサイトの様なアプリです。トップ画面には複数のタブが並べられ、タブごとにサービスの機能を提供しております。

このアプリの開発が始まったのは 2015 年で、私が入社した 2019 年にはすべて Swift への変換は済んでいたものの、下記の様な状態で新規機能開発や保守がとてもしづらい状況になっておりました。

  • 俗にいう FatViewController が多数存在
  • 責務が不明瞭なヘルパークラスなどが多く存在し、共通処理の多くがそれらに押し込まれている
  • ヘルパークラスが他のヘルパークラスをプロパティとして持っていたり、継承関係もあったりと、縦にも横にも複雑に絡みあっている

先述の通り、m3.com アプリは主力アプリで、まだまだ機能開発や保守をつづけていかないといけないのですが、このままではやがて立ち行かなくなるため、2020年秋ごろからこの状況の打開に向けて動き始めました。

新規アプリかリファクタリングか

この状況を打開するために最初に行ったのは、新しくアプリを作り直すか、リファクタリングにするかの検討でした。

正直なところ、既存のコードは辛い部分が多かったので、全部捨てて一から作り直したい気持ちがも強かったのですが、新規アプリの場合「全体の工数の見積もり」「会社からの承認 」「(BundleID を分ける場合)ユーザにアプリを乗り換えてもらう必要がある」「(BundleIDを分けない場合)リリースまでのリードタイムが伸びる」など、想定される業務・課題・リスクが山のようにありました。

そのために、既存アプリの問題点を改めて洗い出し、どのような設計が理想であるかを明確にしたところ、絡み合ってる既存のコードを剥がしながら綺麗に再構築し、細かく刻んでのリリース(アプリのアップデート)ができそうだと判断し、この二択は深く悩まずにリファクタリングを選ぶことができました。

リファクタリングの大まかな設計

ここからが本題のリファクタリングの中身の話になります。リファクタリングに際し、まず下記の大まかな設計と進め方を決めました。

  • MVVM + Flux
  • マルチモジュール化
  • DI

MVVM + Flux

以前から緩いコーディングルールはありましたが、アプリ全体の設計に関するルールはなく、ViewController でビジネスロジックの処理を行うパターンや、ViewModel が介在するパターンなど混在していました。さらにサービス横断で共通処理を行うヘルパークラスを複数抱えるなど、本来関係ないはずの他のサービス同士が互いに絡み合っている状態になっていました。

リファクタリングではこれらを解決すべく、全サービスで MVVM + Flux の形に統一し、ヘルパークラスを排除をしました(後述)。

具体的には MVVM にした上で、ViewModel と Model のやりとりの部分に Flux を導入した形です。Flux 部分は標準の形で取り込んでいるため、ここで改めて説明するより Facebook(現Meta) が提唱している Flux のコンセプトを参照していただいた方が分かりやすいかと思います。

これを踏まえて、m3.com アプリにおける、状態更新/UI更新について軽くまとめます。

状態更新

  • ViewController: UIイベントを ViewModel に伝える。
  • ViewModel: 受け取った UI イベントをもとに、ActionCreator へ処理を依頼。
  • ActionCreator: ビジネスロジック担当。APIコールやそのレスポンスに対して処理を行い、状態の更新命令(Action)を Dispatcher へ送る。
  • Action: Store が持つ状態を書き換えるための情報を持つだけのデータ型。
  • Dispatcher: Store に Action を流すためのパイプ。
  • Store: 状態を保持する。状態の書き換えは Dispatcher 経由で送られてきた Action によってのみ行う。

という形で構成されており、フローは下記の図のようになっています。

f:id:tmnet357:20220309102533p:plain

UI更新

これに加え、iOS では特殊な形ではありますが、

  • ViewController で表示するUIの情報を ViewModel 側に State という形でまとめ、公開する
  • ViewController はこれを購読し State が流れてくるたびに UI の書き換る

という形を取っています。

f:id:tmnet357:20220309115621p:plain
UI更新のフロー

状態と State という単語が混在するせいで紛らわしくなっており恐縮ですが、「Store が持つ状態」はアプリが保持する状態全般を表し、「ViewModel が持つ State」は ViewController のUI に特化した情報、と全く異なるものです。

また、内部ではリポジトリパターン/REST API/ローカルキャッシュなどを利用していますが、Flux の流れをシンプルにするため省いています。

ちなみにリアクティブプログラミングの実現には Swift 純正の Combine を使っていますが、UIについては SwiftUI ではなく UIKit を使っています。別アプリでは SwiftUI で実装しているものもありますが、その時に SwiftUI の制限に悩まされた経験から、m3.com アプリのリファクタリング開始時に SwiftUI を入れるの時期尚早ということもあり UIKit にしました。

マルチモジュール化

リファクタリング前は1つのターゲットに全てのコードがぶら下がっていましたが、依存関係を絞ったりビルド時間を短縮するために、リファクタリングでは下記のような構成でモジュール分割しました。M3ComApp がアプリ本体で、Infra モジュール、Domainモジュールと、各サービス単位の Feature モジュールに分かれています。

f:id:tmnet357:20220307203248p:plain

さらに、リファクタリング前は .xcodeproj ファイルを Git 管理していましたが、リファクタリングでは複数のメンバーが並行して作業を進めるため、そのままでは .xcodeproj ファイルのコンフリクトが多発することが容易に想像できました。そのため、リファクタリングに先駆けて xcodegen (プロジェクトファイルを自動生成するツール)を導入し、.xcodeproj の管理を Git から外して自動生成するようにしました。これより、モジュール追加やファイル追加/削除を気兼ねなく行えるようになりました。

DI

リファクタリング前の m3.com アプリでは DI を行なっておらず、ビジネスロジックのユニットテストで実際に API を叩く実装になっていて「テストの動作が不安定」「テストが書きづらい」という課題があったために、リファクタリングでは DI を導入する形にしました。DI のライブラリはいくつかありますが、チーム内の他のアプリで導入実績もあり、設定ミスをビルド時に検知できるという理由で needle を選びました。

リファクタリングの進め方

方針がある程度固まった後は、「その方針で問題がないか調べる」のと同時に「リファクタリングのサンプルを作る」意味合いも兼ねて、まずは一人のメンバーが一番規模の小さいサービスでリファクタリングを進めます。上記の方針は最初に全て固まったわけではなく、この中で固まっていった部分もありますが、小さい規模のリファクタリングは問題なく進められました。

スモールスタートで1つのサービスのリファクタリングを完成させた後は、それを参考に、規模の大きいサービスのリファクタリングを二人のメンバーが並行して行いました(アプリ上のサービスは全部で20種類以上あるのですが、サービスの形状が似たものでグループ分けすると、大体4〜5グループに分類でき、リファクタリングが難しそうな2つのグループのそれぞれ一番規模の大きいサービスを二人で分担する形です)。小さい規模のリファクタリングは成功することが分かったので、残りのそれらは後回しにして、大きい規模のサービスのリファクタリングで問題が発生しないか、早めに確認したかったためです。

大きい規模のサービスのリファクタリングは骨が折れるものの、幸いにも大きな問題は発生しなかったため、アプリ全体のリファクタリングも大きな問題もなく進めらそうという目処が立ちました。現在、m3.com アプリの新規機能や他のアプリの開発と並行しつつ、リファクタリングを鋭意進めています。

その他のルール

上記の設計に加え、リファクタリングではモダンな考え方・ルールを取り入れていますが、その中からいくつかピックアップしてご紹介します。

  • 異なるデータ同士を構造が似ているという理由でまとめない
  • Helper や Manager といった責務が不明瞭な名前のクラスは作らない
  • ID 型の採用

異なるデータ同士を構造が似ているという理由でまとめない

最近は割と「良いコードとは何か?」という話題も成熟し、広く浸透されてきていますので、アンチパターンとして認識されているかも知れませんが、m3.com は歴史もあるため「異なるけど、似たプロパティを持つデータ同士」を「1つの共通クラスにまとめる」という形が結構残ってました。さらにその共通クラスに対して「どちらのデータであるか」を表すフラグが追加され、共通処理もフラグで分岐だらけになり、後で参画したメンバーが解析・不具合修正・機能改修を行うのが辛い状況になっていました。

リファクタリングではこの状況を避けるために、なんでも受け取れる様な共通のクラス/構造体は排除し、共通化ではなく抽象化によってコードをまとめていくようにしています(このテーマだけで1つのブログがかけてしまうため、ここでの深堀りは避けます)。

Helper や Manager といった責務が不明瞭な名前のクラスは作らない

リファクタリング前の m3.com では、ViewController からロジックを切り分出すために XxxHelper というクラスが多数存在していました。ただ、XxxHelper や XxxManager といった名前のクラスは、責任範囲が不明瞭でいろいろな処理が雑多に詰め込まれがちで、これも後で快適なコードリーディングを阻害する要因になります。そのため、責務が不明瞭なクラス/構造体の名前は NG とし、責務が想像つくような名前をつけることをルールにしています。

ID 型を定義する

リファクタリング前の m3.com アプリは xxxId と言った ID を表すプロパティを Int もしくは String のプリミティブな型で扱っていましたが、ID の受け渡しミスによる不具合が発生していました。そのため、リファクタリングでは全て専用の型を定義するようにしました。

struct XxxId: Codable, Hashable {
    var value: String
}

目先のコード量は増えますが、パラメータの受け渡しミスをコンパイルエラーで検知できるようになり、不具合を事前に防げるようになりました。

ちなみに「毎回構造体を定義するのではなく Generics を使えないか」という検討があったのですが、「ID によっては固定値も定義したい」「全ての ID が必ずデータ型に属するわけではない」「一つのデータが複数のIDを持つ」など Generics が使えないパターンもありました。そのため Generics を使うパターンと別に定義するパターンで実装をわけてしまうと、それによるコストもかかるため、一旦、全て定義する方向に倒しました。ただ「やはり Generics 使える部分は使った方が良いのでは?」という意見もあり、ここはまだ要検討部分です。

参考となるアーキテクチャ

現在の m3.com アプリは Android アプリで推奨されているアーキテクチャに近い形になっています。こちらもとても参考になるため、ぜひ読んでみてください。

developer.android.com

まとめ

以上が、今回のブログでお伝えするリファクタリングのお話になります。

他にもリファクタリングで書きたい・掘り下げたい内容が沢山ありますが、ただでさえ長い内容がさらに長くなってしまうため、一旦ここで切り上げます。長文にお付き合いくださり、ありがとうございました。

We are hiring!

リファクタリングの状況は、iOS 側は全体の 8 割ほど、Android は 6 割ほどの進捗のためゴールまでもう少しかかりそうですが、リファクタリング前に比べてとても保守・運用がしやすくなっているので、今後の開発がとても楽しみです。

今まで、エムスリーのテックブログで、スマホネイティブアプリについて発信頻度が少ないため「ネイティブアプリエンジニアの募集はしてるけど、本当に開発しているの?」と思われる方もいらっしゃるかもしれませんが、このリファクタリングを中心に、その他のアプリ開発もバリバリに行なってます!

カジュアル面談もやっておりますので、このブログの内容やその他のアプリの開発について聞きたい方がいらっしゃいましたら、ぜひお話しましょう!

jobs.m3.com