つばくろぐ @takamii228

知は力なり

アプリケーションが依存するFWやライブラリからアップデート戦略を考える

最近はFlutterでiOS / Androidアプリ開発をしているのですが、ある程度の頻度でプラットフォームやライブラリのアップデートに追従していかないとあっという間に取り残されてしまう空気を感じています。

ネイティブのOSでいうとちょうど先日Android 11のBetaがリリースされたり、来週WWDCiOS 14が発表されたりと、年に一回は必ずアップデートがある領域ではあるのですが、Flutterはそれ以上に短いサイクルでアップデートが入っています。

Flutter自体はstableのリリース頻度を見ても3-4ヶ月に一回のペースで更新があるようです。

flutter.dev

このようにあるFWを使ってアプリを作ってサービスを継続的に提供していく場合、そのFWのバージョンアップにどうやって追従していくべきかなんとなく考えてみたのでまとめてみます。

追従していくおおまかな流れ

だいたいこんな感じで考えればよいのではないか、というのを以下にまとめてみました。

  1. アプリケーションが依存するものを洗い出す
  2. 依存するものの更新ライフサイクルを確認する
  3. FW/ライブラリのバージョンアップ or リプレースを計画する

今回はFlutterを例に順番に考えてみます。

1.アプリケーションが依存するものを洗い出す

まずアプリケーションが構成しているものを順番に洗い出して、それを要素分解して依存関係を明確にしていきます。

洗い出す観点は以下のとおりです。

  • プログラム・設定ファイル
  • SDK・ライブラリ
  • 実行環境
  • 開発環境

Flutterの場合はまず大きく①FlutterのDart部分、②Androidのネイティブ部分、③iOSのネイティブ部分に分けられます。

f:id:takamii228:20200621204050p:plain

①FlutterのDart部分

FlutterのDart部分はFlutterのSDKに含まれるPackagesを参照する部分と、Pluginをimportする部分があります。

Flutter Pluginは参照するときはpubspec.yamlに記述するだけですが、中身は入れ子でFlutterプロジェクトになっていてDartソースはもちろんAndrodやiOSのネイティブコードを含んでいるものがあるので注意が必要です。

開発にはAndroid StudioAndroid StudioのFlutter Pluginを使います(VS CodeIntelliJのケースもあるでしょう)。

f:id:takamii228:20200621204244p:plain

Androidのネイティブ部分

Androidのネイティブ部分はKotlin / JavaのコードやJAR / AARライブラリ、Android SDKのライブラリやGraldeプラグインからなります。ビルドにはGradleが利用され、build.graldeに設定が記述されます。そもそもAndroidにはJDKが必要ですね。

f:id:takamii228:20200621204548p:plain

iOSのネイティブ部分

iOSのネイティブ部分はSwfit / Objective-Cと、CocoaPodsからimportするライブラリ、XCodeのFrameworkなどからなります。ビルドはXCodeに内包されるSDKで実行されます。

f:id:takamii228:20200621204755p:plain

全体像

①ー③を1枚絵にすると以下のようになります。

f:id:takamii228:20200621204848p:plain

依存するもののアップデート頻度を確認する

洗い出したコンポーネントをカテゴリ別に分類し、更新頻度や依存するものをまとめておきましょう。

項目名 カテゴリ 依存定義場所 更新頻度 依存するもの
XCode 開発環境 開発マシン上 メジャーバージョンは年1回
マイナーバージョンは3-4ヶ月に1回
Mac OS
Android Studio 開発環境 開発マシン上 3-4ヶ月に1回 -
Flutter SDK 開発FW 開発マシン上
pubspec.yaml
3-4ヶ月に1回 Android Studio
XCode
CocoaPods
Android SDK 開発FW build.gralde
AndroidManifest.xml
年に1回 Android Studio
Gradle
iOS SDK 開発FW XCodeプロジェクト設定 3-4ヶ月に1回 XCode
Flutterプラグイン 外部ライブラリ pubspec.yaml ライブラリに依存 Android SDK
iOS SDK
Flutter SDK
Android外部ライブラリ 外部ライブラリ build.gradle ライブラリに依存 Android SDK
Gradle
依存ライブラリ
iOS外部ライブラリ 外部ライブラリ Podfile ライブラリに依存 iOS SDK
CocoaPods
依存ライブラリ
Dart Programs ソースコード - SDKに依存 Flutter SDK
依存ライブラリ
Kotlin Programs
(Java)
ソースコード - SDKに依存 Android SDK
Kotlin Version
JDK
Swift Programs
(Objective-C)
ソースコード - SDKに依存 iOS SDK
Swift Version
XCode

Flutterの場合はFlutterプラグインの中で依存関係があったり、更にAndroidiOSのソースが入っていてSDKを参照してたりするのでより注意が必要です。

FWのバージョンアップ、ライブラリのバージョンアップ or リプレースを計画する

上記の表を並べて見てみると大きなアップデートサイクルとして1年単位のOSメジャーバージョンアップとFWのバージョンアップが4半期に一回程度あるので、少なくともこれらを考慮してバージョンアップを計画しておけばよさそうです。

細かい外部ライブラリのパッチやバグ改修には、もしかしたらFWのアップデートが必要かもしれません。また外部ライブラリのみのパッチであれば月次単位でのアップデートでもよさそうです。

OSのメジャーバージョンアップ対応やフレームワークのバージョンアップは影響が広範囲に及ぶため、マイナーバージョンアップと比べて修正箇所が多くなる可能性があるため作業量を多めに見積もっておきましょう。

いずれにしても、どいういう時にどのバージョンアップを計画しておくのか、事前に決めておくことが大事です。決して据え置きにして蓋をすることはやめましょう。あとになって必ず負債化することになります。

アップデートを安全に行うために

更新のバッチサイズを小さくする

フレームワークのバージョンアップを控えていると、それに合わせてライブラリもバージョンアップしておけばいいや、となりがちです。

しかし、アプリケーションのバージョンアップは広範囲であればあるほど影響範囲が広くなり、変更箇所が多くなって対処に時間がかかります。

なのでバージョンアップの回数分コストが発生するためこまめに複数回やるか、一括で一気にやるかとのトレードオフにはなりますが、影響範囲が狭い範囲でバッチサイズを小さくした細かいバージョンアップを定期的に行っておくほうがリスクを低く抑えられるでしょう。

E2Eテストを用意する

FWをバージョンアップするときに、アプリケーションの振る舞いが変わっていないかをFWのDiffから追うことは結構大変です。

FlutterのRelease Noteを見ると、破壊的な変更は説明があるものの、細かいPRを一つ一つ追っていくのはかなり大変です。

flutter.dev

このとき、アプリケーションの外から見た振る舞いをEnd-to-Endテストで記述して自動で実行できるようにしておくと、バージョンアップで壊れている箇所がないかをすぐに確認することができます。また失敗するテストから修正が必要な箇所をすぐに特定できるようにもなります。ただし、重厚に全機能を網羅するテストを用意するのはメンテナンスコストとのトレードオフにもなるため、まずは正常系の画面遷移のパスを網羅する程度のものを準備しておくとよいでしょう。Flutterの場合はFluter drive testでE2Eテストが記述できます。

アップデートに追従しにくいライブラリはリプレースできないか考える

FWの頻繁な更新に対応しようとすると、場合によっては3rdパーティによるメンテナンスが追いつかないケースもあります。そういう場合はFWの更新と天秤にかけた意思決定が必要です。場合によっては違うライブラリに置き換えたり、自前で作成してしまうなどして依存関係を見直すようにしましょう。

プラグインの選定方法に関しては以前記事をまとめたのでこちらも参考にしてみてください。

takamii.hatenablog.com

この段階でいろいろ考えてみてどうしても負債が解消できないとなった場合は、FWを再選定したりアプリケーションを大々的に作り変えることも選択肢の一つになるかもしれません。

アプリケーションの外の依存関係も明らかにしておく

今回はアプリケーション開発・更新にフォーカスしていますが、サービス提供という一つ上の目線で見てみると、その周りにあるものも依存するものとして捉えることができます。単純にヒト・モノ・カネの観点でいうと、関わっているエンジニアや委託している外部ベンダ、マシンや計算リソース、予算管理などが該当します。また社内の運用ルールなども含めて、起こりうる変更に備えておけばよりアジリティ高く変化に対応できるようにできそうです。

まとめ

プラットフォームやフレームワークを選定し外部に公開されているライブラリと組み合わせてアプリケーションを構成するときは、上記のようなアップデートライフサイクルを考慮した開発方針を事前に決めて追加開発の中で計画的に更新していくことで、技術的な負債の発生を未然に抑止し、アプリケーションを継続的に安定稼働させることができるようになるでしょう。初期リリース偏重で、アップデート戦略なしに小手先の末端のソースコードを継ぎ足すだけの機能開発を続けていると、あっという間にコアな部分で負債が発生する未来がやってきてしまいます。

一昔前のSpringやAngularJS、LaravelのLTS版を見てみると数年単位で塩漬けにしていても安泰ではありましたが、今はもうそのような世界はありません。Javaのバージョンアップライフサイクルに代表されるように、ライブラリやFWの寿命が昔に比べて短命になっていると感じます。

このように作って終わり、あとは塩漬けの時代は終わりつつあるので、継続的に依存ライブラリやFWをどう更新してくか、アプリケーション開発の初期段階できちっと考えておきたいものです。