レガシーコードとの付き合い方
こんにちは。予防ヘルスケア×AIテクノロジー(人工知能)に特化したヘルステックベンチャーFiNC TechnologiesでiOSアプリエンジニアをしています施です。台湾から日本来て、今の会社に入社し約2年が経ちました。
始めに
FiNCアプリの「歩数記録機能」は初期の頃から提供している機能で、その時々で様々な改善を繰り返して来ました。そのため、いろいろな問題を抱え込んでしまっている、いわゆる「レガシーコード」のような状態になっていました。
この歩数機能を使った大きな新規開発を行う話が出ていたため、既存のコードが影響して問題が広がらないように、インターフェースの切り分けや関係性の整理などを行いました。
このような問題はよく皆さんも抱えている課題だと思いますので、
今回は、「レガシーコードとどう付き合っていくか」ということについてお話をさせていただきたいと思います。
背景
2020年4月にFiNCアプリホーム画面の大幅リニューアルにより、TOPページに今までなかった歩数、食事、カロリーなどのデータを表示することになりました。下記アプリ画面の上部黒背景のところです。
もともと歩数機能自体は「記録」機能の一つとして存在していて、UX的にもリアルタイム反映できるように作り込まれていました。
今回の新規開発は、このUX的なリアルタイム性も維持しつつ、新たにTOPページにも歩数機能を表示させるという要件となります。
手始めに
チームメンバー質問しながら、歩数コードを一通り調べて情報を整理してみたところ、この図のような構成になっていました:
FiNCアプリにおいて、UIレイヤーにはMVVMを採用していますので、ここにはViewModelが歩数データを扱うFacadeやRepositoryを直接知っていて、歩数機能のView層が密結合となっている状態です。
中でも、歩数をリアルタイムで流しているLocalStepCountFacadeに関しては想定外の挙動をしていることがわかりましたが、View層(StepViewController)でうまく吸収していたために問題なく動いていると言った状況でした。
そのため、コードレベルでこれ以上細かい挙動の問題点を追うことが難しいという判断となりました。
この時点でわかったことを整理してみますと:
- 歩数のレイヤー間での密結合が起こっている。
- レイヤー間の責務分担がちゃんとしていない。
- 歩数のリアルタイム更新を実現するにはLocalStepCountFacadeが必要。
- 新規機能追加時に、にLocalStepCountFacadeをこのまま使うことはできないので、何らかのケアが必要。
このまま拡張していくと問題が更に肥大化するのが目に見えて分かったので、一旦作業を止め、コメントやデバッグ出力をまとめたPRを出してチームメンバーに共有することにしました。
考察
この問題に対して正攻法で対処するには、責務を再考したり、コードを分割したり再構築した上で、きれいなインタフェースを定義し、今回の新しい歩数機能に使ってもらうことになります。端的に言うと歩数基盤を作り直すことになります。
しかし、スケージュール的には大規模なリファクタリングを行う余裕はありません。一方で、歩数のリアルタイム更新を諦めるのはプロダクトのUXを下げるため選択できません。まさに将棋などで言う詰みの状態です。
そこで妥協案を探り出さないといけないという共通認識が出来ました。一旦、既存のコードと新規で作ったものを図に並べてみると下記のように:
レイヤーで区切ると下記のようになります。
ここで妥協案に向けた課題をおさらいします:
- 新規実装(HomeTopStepRepository)でLocalStepCountFacadeを使いたい古い実装で利用されているリアルタイム性が考慮された機能を利用したい。リアルタイム性を考慮した新規実装を行う時間はない。
- 新規実装のメンテナンス性を維持したい
古い実装を利用しつつも、新規実装にレガシーな依存関係を残してしまうと将来的にもレガシーコードの入れ替えが不可能になってしまう。
妥協案
そもそもHomeTopStepRepositoryが直接LocalStepCountFacadeを知るという、レイヤーを跨いだ依存関係を作らないという前提があります。
となると、もっとも愚直な案としてはその間に仲介となる存在を設けることが考えられます。いわゆるアダプターです。その存在がLocalStepCountFacadeを扱い、LocalStepCountFacadeの例外挙動もここで吸収してもらいます。図にすると、下記のOldLocalStepSourceに該当します。
妥当する前提の産物でもあるので、誤用するケースを避けるのに命名に工夫して、Oldつけたり、deprecatedをつけたりします。
修正ロジックなどをどこに入れるか、それを悩む必要がなく、OldLocalStepSourceにまとめれば良いです。こうすることで問題点を一箇所に集中することができました。
しかし、このままではHomeTopStepRepositoryは間接的にLocalStepCountFacadeを依存しているので、新規実装が旧実装に引きずられていることに変わりありません。いつでも簡単に歩数ソースを切り替えられるようにしたければ、きっちり断捨離をやり切ることが必須条件となります。簡単に言うと依存関係を逆転します。
HomeTopStepRepositoryがLocalStepSourceProtocolというルールを決め、外部に従ってもらいます。これによって歩数ソースを切り替えるインタフェースが自然に出来上がります。
これで、新規実装の整合性やメンテナンス性が崩れない、レガシーコードを扱うという2つの条件を満たすことが出来ました。
当然ここにLocalStepSourceProtocolを適合したモックリポジトリを注入できるので、ここでユニットテストも可能です。
まとめ
今回はFiNCアプリの歩数機能、及びホーム画面の新規機能を例として、レガシーコードの取扱方について紹介しました。レガシーコードは何処のどのようなプロジェクトにも必ず抱えている課題です。十分なケアを払いそのレガシーコードに直面し、無理に作り込みせず、チームメンバーとコミュニケーションを取りながら進めて行くのがFiNC流のやり方です。
また、今回の例では、主に責務の分割や依存性の分析などを通して、良い結果まで導き出せました。主に単一責任の原則(SRP)及び依存逆転の原則(DIP)を基にしたからこそ成立することです。
FiNC Technologiesクライアントエンジニア陣は、大規模アプリケーションの品質やメンテナンス性を如何に維持するため、原理原則、コミュニケーションを常に大事にしています。ご興味のある方はぜひ!