フロントエンドでの段階的なコード分割による複雑さの解消

はじめに

kintoneチームの前田です。 kintoneチームはClosureで書かれているフロントエンドのコードを段階的に分割することに取り組んでいました。 その中で複雑さの解消を実感する機会がありました。 この複雑さはClosureに特有というわけでもなく、形を変えてサーバーサイドでも生じそうなものでした。 今回の記事ではそのような複雑さを紹介しようと思います。

ページ単位でのコード分割による複雑さの解消

コード分割とは、kintoneの機能に沿ったパッケージやディレクトリにコードが配置され、コード間の依存も機能に沿って制限された状態にすることです。 このような取り組みの背景やコード分割の仕組み等の詳細は、以下の記事に詳しく記載されています。

blog.cybozu.io

kintoneではページの切れ目が機能の切れ目でもあるため、まずはページ単位でのコード分割が進みました。 たとえば、アプリ設定画面のコード、ファイル読み込み画面のコードというようにコードがディレクトリにまとめられ、 お互いのコードがページをまたいで依存し合わないように管理されました。

ページ単位での分割によって、影響範囲が分かりやすくなる等の形で複雑さの解消を確認できました。 たとえば、アプリ設定画面のコードに手を入れても、変更の影響範囲がアプリ設定画面に閉じることが一目で分かるようになりました。 また、処理の役割分担がおかしくなっていることにも、依存の観点から気づけるようにもなりました。 上記の記事でも紹介されていたように、ページ共通の処理を担当する部分に、特定のページ専用処理が入り込んでしまっていました。 このようなことをコード分割の最中に気づくことができ、今後は紛れ込まないように依存を監視することができるようになりました。

ページ内の機能単位でのコード分割による複雑さの解消

ページ単位での分割でもある程度の複雑さを解消することができましたが、その後ページよりも細かい機能単位での分割が試みられました。 というのもページでは分割単位が粗く、複雑さの解消には更なる分割が必要だったからです。

ページ内の機能単位でのコード分割よる複雑さの解消を実感したのが、レコード編集画面での分割でした。 レコード編集画面は、レコードの編集UI、レコードコメント、レコード編集履歴、JS APIなどといった小機能から成り立つ機能であると考えることができます。 kintoneはフィールドをユーザーが自由に設定できるため、編集UIだけでも多数のコンポーネントから構成されておりそれなりの複雑さになっています。 JS APIに関しては、ユーザーがフィールドの値を編集するとchangeイベントが発火されたり、 kintone.app.record.getkintone.app.record.setのような機能を提供しています。 ページ単位での分割ではこれらの小機能が混ざったままで、本当に複雑な部分に立ち向かうにはまだ切り分けが足りない状態でした。

レコード編集画面の従来のコード構成は下の図のようになっていました。

分割前のコード

レコード編集画面を担当する編集UIコンポーネントからJS API(changeイベントを発火する部分)へ依存があります。 この図では表すことができていないのですが、RecordChangeEmitterのインスタンスを生成するためのセットアップなども編集UIに書かれていました。 つまり、単に依存があるだけでなく、changeイベントを発火する処理の一部が編集UI側に漏れている状況でした。 さらに、JS APIを実行した後に画面を更新するための編集UI側への依存もJS API側から入っています。 このように編集UIとJS APIとの処理同士が微妙に混ざり合い、お互いがお互いを知り合っている状況でした。

分割後のコードは以下の図のようになっています。

分割後のコード

FormUIFormManagerが相互参照している箇所は直っていないのですが、編集UIとJS APIとが分割され、お互いに依存しなくなりました。 JS API側からはemitRecordChangeIRecordGatewayという公開インターフェイスのみ参照可能になりました。 emitRecordChangeはただの関数で、この関数を呼び出すだけでchangeイベントを発火できます。 内部にあるRecordChangeEmitterやこのインスタンスを生成するためのセットアップ手順などは隠ぺいされ、 FormUIがイベントを発火させるために知っておくことが必要最小限に抑えられました。

もう一つの公開インターフェイスであるIRecordGatewayを使った依存の逆転も、分割のための重要な役割を果たしています。 JS API実行後は、JS API側がIRecordGatewayのメソッドを実行することで画面を更新することができます。 このようにIRecordGatewayを介することで、直接編集UIに依存せずにすんでいます。 IRecordGatewayの実装であるRecordGatewayImplは元からあるFormManagerのラッパーです。 FormManagerの全体をJS API側に公開するのではなく、IRecordGatewayという必要最小限な側面、役割だけ公開するという形になりました。 RecordGatewayImplは画面のエントリーポイントであるEditPageコンポーネントが生成し、JS API側に注入しています。

少数のインターフェイスを公開しそれ以外は依存を禁止することで生まれた境界のおかげで、お互いをある程度意識する必要なく理解可能な状態になりました。 分割前はJS API側のコードを読んでいると編集UIが表れてきて、コードの読む範囲がJS APIと関係ない部分まで広がっていく状態でした。 分割後ではJS API側のコードは図中の実線で囲われた中に閉じているため、編集UIが出てくることはありません。 編集UI側にもJS APIが表れなくなるためコードの可読性が上がっています。 分割後はFormUIFormManagerの相互参照に立ち向かえる状況に一歩近づいたといえると思います。

逆に言うと、機能に基づく境界が無いと処理が混ざり合い、ここから複雑さが生まれそうに思います。 境界が無い場合、機能Aのクラス、機能Bのクラスと個別に把握できるものが、機能A+Bのクラスといった形になり肥大化します。 すると、相互に依存してしまう可能性も出てきますし、単体テストも個別に書けなくなってしまいます。 機能実装を個別に把握できなくなり、コードを読む時間がよりかかるようになったり、影響範囲がより広くなってしまいます。 実際、編集UIとJS APIではこのような状況になっていましたし、サーバーサイドにおいてもアプリの利用側とアプリの管理側の機能が混じって肥大化していました。

blog.cybozu.io

ちなみに、依存の逆転やエントリーポイントで依存を注入するといったテクニックをClosureのコード分割に活用した時は、大変不思議な気持ちになりました。 というのも、これらのテクニックはクリーンアーキテクチャで紹介されているもので、サーバーサイドで活用するものだと思っていたためです。 実際、サーバーサイドコード分割でも使われています。 Closureを使ったフロントエンドでも活躍するとは分割を始めた時には思っていませんでした。 依存の逆転はサーバーサイドやフロントエンドを問わず機能の分割に使うことができるものなのかもしれません。

終りに

フロントエンドでの段階的なコード分割による複雑さの解消について紹介しました。 まずはページ単位で、その次にページ内の機能単位で分割することによって、複雑さの解消を確認することができました。 ページ単位での分割では影響範囲が分かりやすくなったり、処理の役割分担がおかしくなっていることに気づけるようになりました。 ページ内の機能単位での分割では、少数のインターフェイスを公開しそれ以外を隠蔽することで、各機能を個別に理解可能になりました。

ページ内の機能実装が分割されていない場合、本来は独立して管理できる様々な機能が混ざり合うようになります。 この状態はコードの理解を難しくし、変更による影響範囲を広げていきます。 これは複雑さを生む原因の一つだと思います。

今回紹介した機能単位でのコード分割よる複雑さの解消は、Closure実装のフロントエンドにのみ有効なものではなく、サーバーサイド等にも当てはめることができるように思います。 React化に集中することになったので今後Closureのコード分割を行うことはありませんが、コード分割自体の取り組みや考え方は様々な場所で適用していきたいと思います。