新 Sensor Cloud におけるロジックの取り扱いとコードの構造
本記事は、以前紹介した新 Sensor Cloud の実装紹介の続編となります。前回は型付けについて解説しましたが、今回はより大局的なコードの構造についてお話します。 blog.tinkermode.jp
MODE Platform API と Sensor Cloud の関係
本題に入る前に、MODE Platform API と Sensor Cloud の関係について説明しておきたいと思います。MODE Platform が Web API により提供しているのは時系列データベースやデバイス管理といった、IoT 開発において一般的に必要となる機能群です。Sensor Cloud を始めとするMODE の各種クラウドサービスは、この MODE Platform API による汎用の機能を土台とし、それを応用して各サービス固有の機能を実現しています。
以前の新 Sensor Cloud に関する記事においてはこの API コールをしている部分の型付けついて解説しましたが、今回は Sensor Cloud 固有の機能のロジック (UI に依存しない部分) がどのようなコードの構造になっているのか、また前述の API コール部分とどのような関係になっているのかを解説していきます。このロジックは MODE Platform の API コールを駆使して実現されていますが、複数の観点を考慮して妥当なコードの形にしていく必要があります。
API コール部分について
まず前提としてこの API コール部分は、各 API と単射になるような関数の集りです。つまり 1 API につき 1 つの関数が実装されており、ただし一部の API については未対応という状況です。複数の API を呼び出すような関数は作っていませんし、もっと言うとレスポンスを加工するような処理も行なっていない、愚直なラッパライブラリといった感じになっています。実際は複数の API コールを組み合わせたり、レスポンスを加工して用いることが大半なのですが、このようにした理由は以下です。
- Sensor Cloud を超えて、将来的に MODE 共通の SDK としての切り出しを可能とする
- 既存の MODE Platform API のドキュメントがそのままコードドキュメントとして参照できる
- 変動性の低いコンポーネントの切り出し
1, 2 は、MODE Platform API を利用する JavaScript/TypeScript の開発を将来的に高速に行なえるようにするための仕込みです。MODE 社内の開発もそうですが、将来的に SDK を外部公開した場合には MODE を活用したサードパーティの開発も効率化が見込めます。
3 はメンテナンス性に関わる部分です。MODE Platform API は外部向けにも公開されている Web API であり仕様変更が非常に起きにくいです。 それらとストレートに対応するコンポーネントであれば、同程度に変更が起きにくくなります。したがって変更時には少なくともこれらのコンポーネントには手を入れる必要がなく、その呼び出し側だけを改修をすれば済むということになります。
Sensor Cloud 固有のロジック
さて、こうなってくると Sensor Cloud 固有のロジックは上記の API コール部分とは完全に分離した形で記述する必要が出てきます。また、こうしたロジックのテスタビリティとコードの見通しを考えると、View からは分離して記述すべきと思います。したがってこうした処理は logic.ts というファイルに切り出すこととしました。
少し脱線します。 この logic.ts と API コール層の関係は、クリーンアーキテクチャで言うところのエンティティとユースケースの関係にあると見ることもできるでしょうが、今回はクリーンアーキテクチャを意識してこの構造にしたわけではありません。前述の通り API コール層を分離したいことと、ロジックを View から分離したいことを考えて、自然とこのように落ち着きました。 加えて、依存方向は必ず View -> ロジック -> API コール層 となるように今回しましたが、これもクリーンアーキテクチャがどうというよりは依存が常に一定方向であることで複雑度を上げずに済むことを考えれば自然なことと思います。 Rich Hickey は Simple Made Easy の中で complect (複数のものの絡まり合い) が simplicity を損いメンテナンス性を下げる (意訳) と述べていますが、依存方向の complect も回避したいというのが個人的な考えです。
閑話休題。
さてこのロジック層ですが、ラフに機能単位でディレクトリ分けしています。以下がそのイメージです。
/ | +- core/ | | | +- api.ts | +- dashboard/ | | | +- logic.ts | +- logic.test.ts | +- view 系コード | +- hardware/ | | | +- logic.ts | +- logic.test.ts | +- view 系コード :
core/api.ts が共通の API コール層です。 アプリケーションの複雑度によってはロジック層をさらに細かく分割・構成するべきかもしれませんが、今の Sensor Cloud で求められているものを考えればこうしてユーザ向けの機能で分割している程度で十分であり、現時点においてこれ以上は過剰になり設計と把握のコストが上回ると判断しました。
また logic.ts は基本的にクラスを使わず、関数 (+ データ構造) による構成をとっています。JavaScript/TypeScript は OOP を強制する言語ではないと考えていますし、規模的にクラスによるモデリングが冗長に感じられたためです。これにより logic.ts は自身のステートを持ちません。
ロジックのテストに関わる構造
さてテスタビリティも考慮して logic.ts を設けているわけなので、logic.test.ts について考えましょう。これはそのファイル名の通り logic.ts に対するテストとなります。 テストが書きにくく UI 改修の起きやすい view 層をさて置いて、Sensor Cloud 固有のロジックについてチェックをしたいわけです。
前節で述べた通り logic.ts は自身のステートを持たない関数の集まりです。ここで発生する副作用は以下であり、テストを書くにあたってはこれらの考慮が必要です。
- Web API コール
- Web Storage の利用
- console によるログ出力
2, 3 については対処が簡単です。JavaScript/TypeScript の特性として組込みオブジェクトのプロパティは簡単に書き換えられますし、テストフレームワークがそれを利用したテストダブルの機能を提供していたりします。今回は jest を使っているので jest.spyOn を利用しました。
さて Web API コール部分についてですが、結論から言うと axios-mock-adapter を利用しました。 当初は core/api.ts に interface を用意しておき、DI することで core/api.ts をモックに差し替えてのテストにすることを検討していたのですが、このためだけに DI を導入せずとも axios-mock-adapter で事足りると考えこの形に落ち着きました。
なお core/api.ts は axios に強く依存しているように感じるかと思いますが、axios 利用部分は core/api.ts 内部の private な関数として抽象化されているため、万が一 axios から別のライブラリへ乗り換えることになっても修正は局所的になります*1。
まとめ
以上、新 Sensor Cloud のロジックに関連したコード構造について説明いたしました。将来の開発速度を犠牲にせず、かつ過剰にならないラインを自分なりに狙ってみたのですがいかがでしょうか。これから MODE のビジネスが発展していくにつれ Sensor Cloud の役割が変化していき適切な構造は変わっていく (あるいは今まで気づいていなかった不適切さが明らかになる) 可能性は当然あるでしょう。その時が来たら、得られた学びから設計・実装を考え直していければと思います。
*1:もちろん axios-mock-adapter を使ったテストコードは全面的に修正が発生しますが、トレードオフとして許容しています。axios の乗り換え自体が発生しにくいであろうと思われるので、Web API モック定義の修正くらいは受け入れます。