読者です 読者をやめる 読者になる 読者になる
無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

ReactとCordovaで、Web/モバイルのハイブリットアプリを作った話

jiji JavaScript

ReactとCordovaを使って、ブラウザ向けのWebUI + Androidで動くスマホアプリ を提供するサービスを、一人で作ってみた話です。

サマリー

  • 作ったもの
  • 最大の課題:作業量
  • 一人で作りきるために意識したこと
  • 取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
  • 取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
  • 取り組み3: ユニットテストを書く
  • まとめ

作ったもの

jiji2.unageanu.net

自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんにFX自動取引を開始できる、システムトレードフレームワークです。

スクリーンショットをいくつか。

Web UI:

f:id:unageanu:20151209113018p:plain f:id:unageanu:20151209113019p:plain f:id:unageanu:20151209113020p:plain

スマホアプリ:

f:id:unageanu:20151209113043p:plain f:id:unageanu:20151209113044p:plain f:id:unageanu:20151209113045p:plain

コード規模

UI側のコード規模は以下の通り。テストケースを含めた合計で、31000行くらいです。

WebUI アプリ 合計
ソースコード 15222 4454 19676
テストケース 9676 1574 11250


また、これとは別に、サーバーのコード(ruby)が、ソースコード/テストケースあわせて、27000行くらいあります。

構成

  • UIは、いわゆるシングルページアプリケーションで、ECMAScript2015 で書いています。
  • マテリアルデザインを採用していて、Reactで動くUIライブラリの Material UI を利用しています。

前置きは以上。

最大の課題:作業量

開発にあたっての最大の課題は、なんといっても作業量でした。
初期の検討段階で、「Webアプリ or モバイルアプリのどちらかに注力できないか?」と考えましたが、

  • Push通知やモバイルでのシステム管理に対応した、スマホ時代のFXシステムトレードフレームワークにしたかった。

    • 同様の機能を提供するソフトはすでにあるのですが、モバイルでの取引状況の確認やアルゴリズムの管理に難があったのが、開発のきっかけにもなっています。
  • とはいえ、スマホで取引アルゴリズムを作成したり、取引結果を分析するのはツラい

    • Rubyのコードを作成するのは、やはりPCからになると思われる。(外出先からスマホアプリで緊急修正、というのはあるにしても)
    • 取引結果の分析も、広い画面で行いたい。

ということで、両方必要という結論に至りました。そうと決まれば、後は、如何にして作りきるかです。

一人で作りきるために意識したこと

以下の2点を、特に意識しました。

  • コードの共通化/再利用
    • Web UI/スマホアプリのコードを可能な限り共通化して、開発コストを削減する。
  • ユニットテスト
    • ユニットテストで個々のモジュールの動作を保障。
    • 機能追加や変更を、低コストで素早く行えるようにする。

そして、↑のための具体的な取り組みとして、以下を行いました。

  1. Cordovaを使って、Web UI/スマホアプリのコードを共通化する
  2. レイヤードアーキテクチャを採用し、共有できるコードを最大化する
  3. ユニットテストを書く

取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する

スマホアプリをCordovaを使ったハイブリットアプリにして、Web UI/スマホアプリのコードを共通化しました。

  • Cordovaを使うことで、JavaScript + HTML + CSSスマホアプリを作ることができるので、同様の構成で作成するWeb UIのコードを再利用できるようになります。
  • React Native という選択肢もありましたが、UI側の開発に着手した時点ではAndroidには対応していなかったため、見送りました。
    • Cordovaは、Webでの情報が豊富で、実績も多くあった点がプラスでした。
    • また、Push通知や課金決済関連のプラグインがあったことも大きいです。
  • 採用に当たって懸念だった、UIのパフォーマンスについては、かんたんなプロトタイプを作って検証しました。
    • やはり、ネイティブアプリよりはもっさりしていますが、ゲームのようなパフォーマンスがアプリの価値に直結する性質のソフトウェアではないので、問題ないレベルと判断。
    • それよりも、リリースまでの開発工数を抑えること、また、リリース後の機能強化や改善をスピード感を持って行えること、を重視しました。 unageanu.hatenablog.com

「UIの反応速度とか使い勝手とか、実際どんなもんなの?」と気になる方は、ダウンロードしてお試し頂ければと思います。(まいど、ありがとうございます! / 30日の無料トライアル期間内に、定期利用を解除すれば請求は発生しないので大丈夫です)

なお、Crosswalkの効果は絶大でした。体感速度が劇的に向上したうえ、怪しい動きをしていた箇所も治ったり。 apkのサイズ/起動時間は増えますが、採用する価値はあります。

取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する

MVVMのレイヤードアーキテクチャを採用して、Modelレイヤのコードを共通化。
ViewやViewModelも、変更の度合いに応じて最適なレイヤーでカスタマイズすることで、共有できるコードを最大化しました。

f:id:unageanu:20151209145903p:plain

  • Model/View/ViewModel + 通信などのInfrastructureで構成。
  • Model(UIに依存しない、アプリ共通のコアドメイン)は、Web UI/スマホアプリでそのまま共有。
  • スタイルの変更だけで済む場合は、CSSのレベルでカスタマイズ。
  • DOM構造を変える必要がある場合は、View(Reactコンポーネント)のコードを差し替えて対応。
  • Viewで管理するデータや機能がそもそも異なる場合は、ViewModel(Modelをラップして、Viewに依存するデータや操作を提供するクラス群)のレベルで、カスタマイズして再利用。
    • 例えば、WebUIでは、"通知の一覧を表示する機能"と"選択した通知の詳細を表示する機能"を同じ画面で提供していますが、スマホアプリでは別画面にしています。 このため、WebUI版では、通知の一覧画面のViewModelで通知一覧と選択状態を管理していますが、スマホアプリ版では2つのViewModelで管理する形に変更しています。

このほか、Infrastructureの通信部分やGoogle Analyticsでの利用状況解析も、WebUI/スマホアプリでカスタマイズして利用しています。

  • 通信部分は接続先REST APIを差し替える必要があるため(Web UIは自ホストに、スマホアプリはUIで設定したサーバーに接続)、URLResolverとして機能を切り出しカスタマイズしています。
  • 利用状況解析は、スマホアプリではJava APIを使うようカスタマイズしたクラスを用意して差し替え。

以下は、利用状況解析のコードの抜粋です。 インターフェイスを統一しつつ、実装を差し替える形で、別のクラスを用意しました。

WebUI版:

export default class GoogleAnalytics {

  // 略
  
  // Google Analytics のJavaScript版APIを呼び出す
  sendEvent( action, label="", value={} ) {
    this.run((ga) => ga('send', 'event', this.category, action, label, value));
  }

  sendTiming( category, timingVar, time, label ) {
    this.run((ga) => {
      ga('timing', category, timingVar, time, label);
    });
  }
  
  // 略
}

スマホアプリ版:

export default class GoogleAnalytics {

  // 略
  
  // Cordova google-analytics-plugin のAPIを呼び出す
  // https://github.com/danwilson/google-analytics-plugin
  sendEvent( action, label="", value={} ) {
    this.run((ga) => {
      ga.trackEvent(this.category, action, label, value,
        ()  => {},
        (e) => console.log(e));
    });
  }

  sendTiming( category, timingVar, time, label ) {
    this.run((ga) => {
      ga.trackTiming(category, time, timingVar, label,
        ()  => {},
        (e) => console.log(e.message));
    });
  }
  
  // 略
}

これら、コンポーネントの差し替えは、DI(Dependency Injection)コンテナを利用して、行うようにしました。 DIコンテナを使うことで、コンポーネントの外側で、環境ごとにどのクラスを使うのか宣言的に指定できるようになります。

WebUI版のコンポーネント定義:

binder.bind("googleAnalytics").to("utils.GoogleAnalytics")
  .withProperties({
    category: "web-ui",
    version:  "1.0"
  }).onInitialize("initialize");

スマホアプリ版のコンポーネント定義:

// スマホアプリ用のクラスを使うように変更
binder.bind("googleAnalytics").to("app.utils.GoogleAnalytics")
  .withProperties({
    category: "app",  // category, versionもカスタマイズ。
    version: "1.0.6"
  }).onInitialize("initialize");

利用側のコードは以下のような感じ。Injectとしてマークしておくと、コンポーネント定義で宣言されたコンポーネントが注入されるので、利用側はAPIを呼び出すだけです。

export default class RmtService extends AbstractService {

  constructor() {
    this.urlResolver     = ContainerJS.Inject;
    this.xhrManager      = ContainerJS.Inject;
    this.googleAnalytics = ContainerJS.Inject; 
    // コンポーネント定義で宣言されたコンポーネントが注入される
  }

  // 略

  putAgentSetting(settings) {
    this.googleAnalytics.sendEvent( "put rmt agent setting" ); // GAにイベントを送信
    return this.xhrManager.xhr(this.serviceUrl("agents"), "PUT", settings);
  }

}

取り組み3: ユニットテストを書く

MVVMアーキテクチャを採用したことで、View以外の部分はDOMに依存しなくなるため、容易にユニットテストができるようになります。 ユニットテストを意識的に用意したことで、一人でも何とか作りきることができたかな、と感じます。

  • 一人で開発したとこもあり、サーバー側の開発で1か月くらいrubyのコードばかり書いていると、自分が書いたコードでも忘れてしまって、変なバグを埋め込んだりします。テストコードがあることでデグレードが検知され、バグに気付くことが多くありました。
  • また、コードがどう動くか、についても、テストケースをみると思い出せるということもありました。

ユニットテストで確認された"動く"コードを少しずつ積み上げていく、という作り方は、心理的にも良かったと思います。 一人なので、開発期間はどうしても長くなりますし、やってもやってもタスクがなくならない、終わりが見えない感じになりますが、"動く"コードが積み上がっていくことで、毎日少しずつでもコードを書いていれば、いつか完成すると思えるようになります。

なお、初版リリースの段階ではViewのテストは省略しました。

  • 初版リリースでは、UIの精緻化などでDOMの構造を変更する可能性が高く、テストを作成しても、効果が変更コストに見合わないと考えました。
  • また、Viewの機能は「ViewModelの状態に応じて仮想DOMを生成する」だけで、テストすべきロジックが少ない && ViewModelのテストで、アプリケーションがこういう状態の時はこうなる、〇〇操作を実行するとこういう状態になる、といった機能は確認できている、というのも理由としてあります。
  • 同じ理由で、E2Eテストも行っていません。このあたりは、今後、サービスが利用されるようになって、メンテナンスの比重が大きくなってからの課題と考えています。

まとめ

  • Cordovaを使ってちゃんと作れば、一人でもサービスを作れる。
    • プロトタイプや最初のMVPは、Cordovaで素早くコストをかけずに作る、そして、手ごたえがあったらネイティブで作り直す、という選択肢は割とアリなのではと思います。
    • もちろん、パフォーマンスが重要なアプリの場合は、不相応な場合もありますが。
  • ユニットテスト重要。テスタビリティは最初から考慮して作ろう。
    • ユニットテストを用意することで、変更ややり直しのコストを最小にでき、トータルの開発コストを大きく削減できます。
    • ユニットテストで確認された"動く"コードを少しずつ積み上げていく、という作り方は、心理的にも効果大です。
  • Crosswalk速い。使おう。