kintone開発チームの取り組み: フロントエンドのコード分割

kintone 開発チームの内山です。この記事では、kintone 開発チームで行っている改善活動の一つであるフロントエンドのコード分割について紹介します。最初にコード分割とはどのような活動なのかを述べ、分割の手順、分割後に得られた学びを順に紹介します。

サーバーサイドのコード分割活動も行っており、カンファレンスやブログで発信しているため、興味がある方は合わせてご参照ください。

blog.cybozu.io speakerdeck.com

コード分割とは

コード分割とは、モノリス化してしまった kintone のコードを機能ごとに分割し、ツールを使って依存関係を監視することを指しています。

本記事では、コードが分割されている状態とは、以下の状態と定義します。

  • kintone の機能ごとにパッケージが切られコードが分割されている
  • 機能間で関わりがなければ、コード間も関わりがなく、依存関係がない

CSVファイル書き出しページとアプリ設定ページのコード同士は依存することを禁止する図
ページ間でコードが依存することを禁止

基本的にパッケージ内で依存が完結するような状態を目指します。 そのため、パッケージ内のコードはパッケージ外から参照されることがなく、パッケージの外側から内側に参照したい場合は共通部分にインターフェースを用意し、それを経由することでのみ参照するようにします。 本記事では、ページの単位を機能の単位と考え、ページ単位で分割しています。 分割したコードは、ファイル単位で依存関係の増減を CI を用いて監視を行い、依存関係の変化を検知出来るようにしています。

コード分割を始めたきっかけ

活動し始めたきっかけとしては、機能開発時に依存関係が複雑に絡み合っていることが原因となり、機能追加がしにくい状況だったため、有志のメンバーでリファクタリングを始めました。 機能開発時に影響範囲の確認にかなり時間を要していることと、機能修正時に意図しない機能にまで影響が及んでしまう問題をどうにかしたいと思いました。

最初は、開発時に問題があったコンポーネントに対してインターフェースや依存関係を整理するリファクタリングを行いました。しかし、これでは一時的な改善にしかならず、開発を続けているうちに依存関係が複雑に入り組んだモノリスな状態に戻ってしまう可能性が高いと思いました。

依存関係の複雑化を抑制する方法を検討した結果、 以下の 2 つの理由からパッケージを分割し、依存関係を監視するという案を進めることにしました。

  1. 既存の構成をあまり変えずに改善をトライできる
  2. 今後の技術刷新時に負債になりにくい

既存の構成をあまり変えずに改善をトライできる

コード分割の手法を検討していく中で、依存関係が意図せず増加しないような仕組みの検討を行いました。しかし、共通コード(複数箇所で使われることを想定して作られたコード)の扱い方により、構成が複雑になるのは避けられませんでした。構成が複雑になることで他メンバーの認知負荷が向上してしまい、日常の開発への影響が懸念されました。また、元の状態に戻したい時に戻しにくいという懸念もあげられました。 構成を大きく変えるのは初手のアプローチとしてはやりすぎだと思ったため、ページごとにパッケージに分割し、CI で依存関係の差分を監視する手法でトライしてみることにしました。

今後の技術刷新時に負債になりにくい

現在 kintone チームでは機能開発と並行して、Closure Library から React.js へ移行しています。詳しくはブログ記事になっているので興味がある方は見ていただければと思います。 フロリア 「React へ置き換えをするならコード分割しなくても良いのではないか」と思われた方もいらっしゃるかもしれません。現状では、依存関係が複雑に絡み合っているため、部分的に React に移行しようと思っても芋蔓式に対象が増えてしまいます。その点で、コード分割により依存関係を整理し、ページごとの凝集度を高めておくことで、既存の仕様を把握しやすくすることは、意味のある単位で部分的に React 化が行いやすくなると考えています。このようにコード分割は技術刷新や改善活動の負債になりにくいと考えており、React 移行完了を待たずに並列で作業を進めています。

コード分割の手順

実際にコード分割する手順としては、以下の順番で行います。

  1. 分割対象のページを選択する
  2. パッケージに分割する
  3. 絡み合った依存関係を解きほぐす
  4. 依存関係を監視する
  5. 動作確認、試験、マージ

1. 分割対象のページを選択する

まず、分割する対象ページを選択します。手始めに比較的分割しやすいところから分割した方が複雑なコードベースを紐解いていくことが出来ると考え、他のページからの依存が比較的少ないCSVファイル書き出しページから分割することにしました。

2. パッケージに分割する

対象が決まったらコードをページ単位ごとにmodulesパッケージ配下へ移動させます。

ファイル書き出し画面のJSファイルを置く場所がsrc/js/kintone直下から分割後にsrc/js/kintone/modulesに変わったことを表す図
分割前後のディレクトリ構成

Closure Library にはネームスペースという概念があり、ドットで区切られたパスでネームスペースを定義出来ます。

// ネームスペースの例
// kintone.app というネームスペースを modules.app に置換したい
kintone.app.ExportPage = class {
  ...
}

ネームスペースはパッケージ構造に沿っているため、ファイルを移動させた後はネームスペースの変更を行います。IDE のリファクタリング機能では Closure Library と相性が悪く、分割対象とは別コンポーネントの同名メソッドまで変更されてしまうことがありました。 また、文字列の一括置換機能で一度に変更する方法も試しましたが、置換を実施したくないケースがあり、置換結果を目視で一つ一つ確認する必要がありました。

前提として、kintone では、多言語表示に対応するために複数の言語ごとに文言キーと文言がペアになったリソースファイルを用意しておき、JavaScript などのコード上では文言を表示したい場所で文言キーを指定します。コード実行時には文言キーをもとにリソースファイルから辞書引きすることで言語ごとの文言を画面へ表示しています。 また、文言キーを使用するコンポーネントのネームスペースを先頭に付与することでキーの重複を避ける運用をしています。 ただ、複数の箇所で使われる文言などは汎用的なことを表すような文言キー名になっているため例外は存在しています。

日本語のリソースファイルの例

# <文言キー>=<文言> の組み合わせになっている
kintone.app.ExportPage.title=ファイル書き出しページ
kintone.app.ExportPage.submit.label=書き出す

文言キーはコード中で文字列結合によりキーを生成している箇所もあるため、一括で置換したときに修正が漏れる可能性があります。 これらを踏まえて、コードを読んでいる時にネームスペースと文言キーが異なるため多少混乱が生むことが予想されますが、リソースファイルから辞書引きが出来れば画面表示に影響はないため今回は文言キーは置換しないことにしました。

よって置換対象を一箇所ずつ目視で確認する方法しか思い浮かばず、置換候補は数百ファイルになることも多いため、かなりの時間を要すことが予想されました。 そこで、ネームスペースのみを一括で置換するリネーム支援ツールを作成しました。

実装としては、jscodeshiftという Codemod ツールを使用し、構文解析をした上でネームスペースとして使われている部分だけを文字列置換するようにしました。 AST Explorerを使用しながら、Closure のコードがどのような AST になるかを理解しながら進めました。

3. 依存関係を解消する

コード分割の方針としては、移動後のパッケージ内からパッケージ外への方向の依存は許容していますが、逆向きのパッケージ外からパッケージ内への依存は禁止する方向で進めています。

共通コードから分割したコードへの依存は禁止するが、分割したコードから共通コードへの依存は許容することを表す図
分割したコードから共通コードへの依存は許容する

そのため、移動後に移動したコンポーネントを呼び出している箇所の依存解消を行います。 現状では、一箇所ずつ目視で確認しています。

コンポーネントの呼び出しを順に辿っていき、パッケージ内のコードからのみ依存されている場合は呼び出している箇所のコードも一緒にパッケージ内に移動することが可能です。呼び出しの経路のどこかでパッケージ外から参照されていた場合は、そのコードの依存を外してパッケージ内に移動すべきかどうかを判断します。 まず、開発者のメンタルモデル的にパッケージ内にある方が自然である場合は依存を外す方法を検討します。パッケージ内になくても良いと判断した場合は、共通コードとして移動せずにそのまま置いておくか、冗長になることを許容しファイルをコピーしてパッケージ外と内にそれぞれ配置しています。

実際に依存を外した例を紹介します。 コード分割する前の状態としては、ファイル書き出しページから LayoutMakerFactory というコンポーネントが呼び出されており、アプリ設定ページからは FormMakerFactory というコンポーネントがそれぞれ呼び出されていました。 アプリ設定ページではフォームに配置するフィールドを設定するため、field.Time のように kintone で扱うことが出来るフィールドタイプの依存が含まれていました。LayoutMakerFactory の中で、アプリ設定ページに関する処理が行われている関係で、FormMakerFactory の依存が入ってしまい、その結果、ファイル書き出しページでは使用していない field.Timeform.table.Row などの依存が漏れ出してしまっていました。また、FormMakerFactoryLayoutMakerFactory を継承しており、スーパークラスからサブクラスを呼び出しているためかなり怪しい状態だと思いました。

ファイル書き出しページのui.LayoutMakerがアプリ設定ページのform.field.Rowを呼び出していることで、依存が漏れ出していることを表す図
ファイル書き出しページにアプリ設定ページの依存が漏れてしまっている

改修方法としては LayoutMakerFactory からアプリ設定ページに関する処理を切り出し、アプリ設定ページからしか使っていないコンポーネントを modules 配下に移動させました。 その結果、LayoutMakerFactory ではアプリ設定ページのことを考慮しなくても良くなり、アプリ設定に関する処理が FormMakerFactory に集約されることでコードの凝集度が高まりました。

ui.LayoutMakerはmodules/export配下に、appsetting.FormMakerはmodules/appsetting配下に移動して依存を分離していることを表す図
LayoutMakerとFormMakerを分離

4. 依存関係を監視する

依存関係が整理されたら、CI で依存関係の増減を監視していきます。 Google Closure Library には標準で依存関係解決ツールがあり、それぞれの JavaScript ファイルに記載した goog.requiregoog.provide というメソッドから依存関係を抽出します。この依存関係を抽出したファイルを解析し、再帰的に探索することであるネームスペースが依存しているファイルをリストアップするスクリプトを作成しました。 このスクリプトの出力をバージョン管理しておき、git push した時点のコードの依存関係と差分を取って依存関係の変化があったときに CI のジョブがエラーになるようにしました。

# ファイル書き出しページの依存関係をリストアップした様子とCIでエラーになる場合の例
src/js/kintone/export/charset.js
src/js/kintone/export/exportpagebase.js
src/js/kintone/export/separator.js
+src/js/kintone/app-setting/tutorial/dialog.js <- アプリ設定ページのチュートリアル機能の依存が増加しているのでCIでエラーになる

依存関係に差分があった場合はエラー内容を確認し、意図した箇所にのみ変化があればバージョン管理しているファイルを最新の状態で上書きします。意図していない箇所に依存が増えていた場合は、依存が入らないようにコードを修正します。

5. 動作確認、試験、マージ

最後に手動で動作確認、自動テストの実施、QA エンジニアの試験によって外部仕様に影響を与えていないかを確認してマージとなります。

分割後の変化・学び

最後にコード分割後にチーム内に起きた変化や学びを紹介します。

影響範囲の確認が楽になった

コード分割前はコード量が膨大なことに加え、Closure Library のネームスペースと IDE の相性の悪さから使用箇所の特定が難しく、影響範囲の確認作業にかなりの時間を要していました。 コード分割後は、分割したコードの修正であれば、パッケージ外から使われることはないため、パッケージ内のみ影響を確認すれば良いことになります。 実際にチームメンバーからも影響範囲の確認が楽になったという声もありました。

ページごとの凝集度が高まり、複雑度が低下し、可読性が向上した

依存関係の解消で前述したファイル書き出しページとアプリ設定ページのケースですが、元々LayoutMakerFactoryは複数のページから使われることを想定した共通コンポーネントだったと思うのですが、改修の過程で特定のページでしか使われない条件分岐が追加されてしまいました。 このような状態になってしまうと、共通クラス内で考慮すべきことが増えるので複雑度が高くなります。また、特定のページに関する処理が点在してしまい、凝集度が低くなり見通しが悪いです。 そもそもこのような依存関係は循環参照になってしまうため、ビルド時にエラーになるので気付けるのですが、Closure Compiler ではアノテーションコメントによって回避することが可能なため、開発時の事情などにより、このような状態になり得る状況です。 この出来事からの学びとしては、このようにスーパークラスからサブクラスへ依存が入っている状態を放置してしまうと、コードの凝集度が低下し、複雑度が高くなってしまい、気付かぬうちに理解が難しくなってしまうということです。 コード分割では、ページごとにコードを分割していくため、このように共通クラスから特定のページへ依存が入っている箇所を発見しやすいと思っています。 また、この依存関係を解消する手法は、前述のサーバーサイドコード分割の記事でも用いていたものだったため、言語が異なっていても同じ手法を適用出来るのは興味深いと思いました。

モノリス化の拡大を防ぐ

分割作業を進めても並行して機能開発は進んでいくため、モノリス化の拡大の方が進んでしまうと分割の効果を感じにくくなってしまいます。モノリス化の拡大が進んでしまう原因としては、メンバーの入れ替わりや時間が経つにつれて当初の設計が失われてしまい、機能追加に応じて再設計が必要な場面でも無理やり機能追加してしまうのが一因ではないかと思っています。 そこで、コード分割活動では、基本的にペアプログラミングかモブプログラミングを行い、複数人で議論しながらタスクを進めているため、コードの設計について属人化が起こりにくくなり、モノリス化の拡大を防げるのではないかと思いました。そのため、コード分割活動以外の通常の機能開発時にも自然とページの境目を意識出来るようになったと感じています。

今後

現在は React.js への 移行活動が本格化しており、そちらに注力するためコード分割活動は一旦ストップしています。(サーバーサイド側の活動は継続しています。)