LoginSignup
11
8

More than 5 years have passed since last update.

Cycle.js のドライバーの開発

Last updated at Posted at 2016-03-02

このドキュメントは http://cycle.js.org/drivers.html の翻訳です (2016年3月2日時点)。

このドキュメントサイト全体を通して、広い範囲でドライバーを使ってきました。DOM ドライバーがもっとも共通のものですが、HTTP ドライバーも使われました。

ドライバーとは何かいつ使うべきかのでしょうか?独自のドライバーをつくるべきときはいつでしょうか、そしてそれはどのように動くのでしょうか?この章では、いくつかの問いかけをします。

ドライバーは関数です。Observable シンク (入力) をリスニングし、手続き的な副作用を実行し、Observable なソース (出力) を返します。

ドライバーは JavaScript における手続き型の副作用をカプセル化するための手段です。経験則は次のようになります。doSomething() のような JavaScript 関数があるなら、ドライバーに含まれるべきです。

もっともよく使われるドライバーである DOM Driver を分析することで、ドライバーが何をするのかを学びましょう。

「ドライバー」という名前はなぜ?

Haskell 1.0 Stream I/O、および Cycle.js も同じように、プログラムの main 関数と Haskell の os 関数のあいだには循環的なインタラクションがあります。オペレーションシステムにおいて、ドライバーはハードウェアデバイスを使うためのソフトウェアインターフェイスであり、外の世界における副作用を引き受けます。

Cycle.js において、アプリケーションを囲む実行世界のための「オペレーションシステム」として考えることができます。おおまかに言えば、DOM、コンソール、JavaScript と JS API は Web のためのオペレーションシステムを想定します。我々はブラウザーと Node.js のようなほかの環境とのインターフェイスをもつソフトウェアアダプターを必要とします。Cycle.js ドライバーは外の世界 (ユーザーと JavaScript 実行環境) と Cycle.js のツールで構築されたアプリケーションの世界のあいだのアダプターとして存在します。

DOM Driver

DOM Driver は Cycle.js においてもっとも重要でもっとも共通のドライバーです。インタラクティブな Web アプリを構築するときに、Cycle.js でもっとも重要なツールでしょう。実際、Cycle Core は約100行のコードであるのに対して、Cycle DOM は少なくとも5倍の規模のコードになります。

主な目的はブラウザーを使うユーザーへのプロキシです。ダイアグラムが示すように、概念的に human() 関数の存在を前提として取り組むことにします。

human-computer-diagram2

しかしながら、実際のところ、domDriver() をターゲットとする main() 関数を書きます。ブラウザーとインタラクトするユーザーのために、main() を DOM とインタラクトすることだけが必要です。何かをユーザーに示すことが必要な場合、代わりに、DOM にそれを表示し、ブラウザーとともに DOM は我々のユーザーにそれを示します。ユーザーのインタラクションイベントを検出する必要がある場合、DOM にイベントリスナーをアタッチし、ユーザーがコンピューターのブラウザーとインタラクトするときに DOM は私たちに通知します。

main-domdriver-side-effects

DOM を通した外の世界とのインタラクションの2つの方向があることに注目してください。書き込み効果はユーザーのスクリーンに表示できる DOM 要素への仮想 DOM の VTree のレンダリングです。読み込み効果はコンピューターを操作するユーザーによって生成された DOM イベントの検出です。

domDriver() はこれら2つの効果が main() 関数と接触することを認めながらこれらを管理します。domDriver() への入力は書き込み効果のためのインストラクションをキャプチャし、読み込み効果は domDriver() の出力して公開されます。domDriver() 関数を解剖した解説は大まかに次のようなものです。

function domDriver(vtree$) {
  // DOM 要素をつくるためのインストラクションとして vtree$ を使う
  // ...
  return {
    select: function select(selector) {
      // 2つのフィールドをもつオブジェクト: observable
      // と events() を返す。前者の observable、
      // は DOM 要素の Observable で任意の
      // selector にマッチする。events(eventType) 関数は
      // selector によってマッチした要素で起きた
      // eventType DOM 要素の Observable を返す。
    }
  };
}

入力の vtree$main() からの出力であり、domDriver() の出力は main() への入力です。

function main({DOM}) {
  // DOM.select(selector).events(eventType) を使う
  // ...
  // ともかく vtree$ をつくる
  // ...
  return {
    DOM: vtree$
  };
}

要点です。

  • main(): ソースを入力としてとり、シンクを返す。
  • domDriver(): シンクを入力にとり、読み書きの効果を実行し、ソースを返す。

副作用を隔離する

ドライバーは何かの副作用と常に関連づけされなければなりません。これまで見てきたように、DOM Driver の主な目的がユーザーをあらわすことであっても、書き込みと読み込み効果をもちます。

JavaScript において、副作用をもつ main() 関数を書くことを妨げるものはありません。単純な console.log() はすでに副作用です。しかしながら、main() を純粋に保ち、テストしやすさや予想しやすさなどの恩恵を手に入れるには、すべての副作用をドライバーでカプセル化することがよりよい方法です。

たとえば、ネットワークリクエストのためのドライバーを想像してください。ネットワークリクエストの副作用を隔離することで、アプリケーションの main() 関数はアプリに関連するビジネスロジックにフォーカスできます。そして、外のリソースとのインターフェイスへの低水準のインストラクションにフォーカスしなくてすみます。このことによってネットワークリクエストをテストするためのシンプルなメソッドが可能になります。実際のネットワークドライバーをフェイクのネットワークリクエストに置き換えることができます。ネットワークドライバーをまねする関数であることおよび、アサーションをつくることが必要なだけです。

何らかの方法で外の世界への影響がないドライバーをつくることを避けてください。とりわけ、ビジネスロジックを含まないドライバーをつくらないでください。これはもっともよくあるコードの臭いです。

ドライバーは効果へのインターフェイスであることに専念すべきであり、通常、Cycle.js アプリが異なる効果を実行することを可能にします。ときには、ライブラリの代わりに、ワンライナーのドライバーを即座につくることができます。たとえば、次のコードはシンプルなロギングドライバーです。

Cycle.run(main, {
  log: msg$ => { msg$.subscribe(msg => console.log(msg); }
});

読み込み限定と書き込み限定のドライバー

たいていのドライバーは、DOM ドライバーのように、シンクをとり、 (書き込みを記述するため) ソースを返します (読み込みをキャッチするため)。しかしながら、書き込み限定のドライバーと読み込み限定のドライバーのための妥当な事例紹介をしましょう。

たとえば、上記のワンライナーのログドライバーは書き込み限定のドライバーです。関数は Observable を返さず、単に受け取ったシンクの msg$ を消費しているだけであることにご注目ください。

ほかのドライバーは main() にイベントを排出するソースの Observable をつくるだけですが、 main() からのシンクを取り込みません。このような事例は読み込み限定の Web Socket ドライバーになります。おおまかな説明は次のようになります。

function WSDriver(/* no sinks */) {
  return Observable.create(observer => {
    const connection = new WebSocket('ws://localhost:4000');
    connection.onerror = (err) => {
      observer.onError(err)
    }
    connection.onmessage = (msg) => {
      observer.onNext(msg)
    }
  }).share();
}

ドライバーのつくりかた

(ワンライナーではない) ドライバーをつくりライブラリとして公開するというあきらかな意図があれば、このセクションのみを読むべきです。典型的には、Cycle.js アプリを書くとき、独自のドライバーをつくる必要はありません。

ドライバーがどちらの副作用をもつか注意深く考えてください。読み込みと書き込み効果の両方をもたせることはできるでしょうか?

読み書き効果の計画を精密に立てたら、どれだけ多様な姿があるのか考えてください。共通の事例をエレガントにカバーして共感できる API をつくります。

ドライバー関数への入力は単一の Observable が想定されます。main() でシンクオブジェクトを返すときに使うアプリ開発者のための実践的な API です。DOM Driver がどのようにして単独の vtree$ Observable を入力としてとり、virtual-dom からの VTree がいかに洗練され、表現豊かであることにご注目ください。一方で、JavaScript オブジェクトを Observable にエミットされる値として常に選んではなりません。意味をなすときにオブジェクトを使い、API を過剰に一般化するよりもシンプルに保つよう心がけてください。オーバーエンジニアをしてはなりません。

2番目に、オプションとして、ドライバー関数の引数として、driverName を期待できます。Cycle.run() でドライバーオブジェクトを通して登録されるときに、ドライバーが何の名前が提供されたのかを知ることが必要な場合にこの引数が使われます。ゆえに、ドライバー関数のシグネチャは次のようになります。

function myDriver(input$, driverName /* optional */)

ドライバー関数の出力は単独の Observable もしくは Observable のクエリ可能なコレクションになります。

単独の Observable が出力のソースであり、Observable によって排出される値が多様である場合、(RxJS filter() オペレーターを使って) それらの値をかんたんにフィルター可能にするとよいでしょう。Observable をフィルタリングしやすいように API を設計し、ドライバー関数に提供されるものがシンク Observable であることを念頭に置いてください。

場合によって、単独の Observable の代わりに、Observable のクエリ可能なコレクションを出力することが必要になります。Observable のクエリ可能なコレクションはたとえば、get(param) のようなパラメーターにもとづいた特定の Observable を選ぶために使う関数をもつ JavaScript オブジェクトに必然的になります。

DOM Driver、たとえば、Observable のクエリ可能なコレクションを出力します。コレクションは実際のところ遅延評価です。events() の呼び出しの前に存在する select(selector).events(eventType) によって出力される Obseravble は存在しません。これは DOM のすべての要素の可能なすべての部分とのために Observable を生成する余力がないからです。かなりの量 (おそらくは無限の) の Observable を含むときには、DOM Driver からの Observable の遅延評価でクエリ可能なコレクションからインスピレーションを得るとよいでしょう。

ドライバーの実装例

Sock と呼ばれるフェイクのリアルタイムチャネル API を想定します。リモート peer に接続し、メッセージを送信し、プッシュベースのメッセージを受信することができます。Sock のための API は次のとおりです。

// peer への接続を確立する
let sock = new Sock('unique-identifier-of-the-peer');

// peer から受け取ったメッセージを購読する
sock.onReceive(function (msg) {
  console.log('Received message: ' + msg);
});

// peer に単独のメッセージを送信する
sock.send('Hello world');

Sock のためのドライバーを構築するにはどうしたらよいでしょうか?効果を特定することから始めます。書き込み効果は sock.send(msg) であり、書き込み効果は受信したメッセージのためのリスナーです。sockDriver(sink)send(msg) の呼び出しを実行するインストラクションとしてシンクを引数にとります。sockDriver() からの出力はすべての受信したメッセージを含むソースでなければなりません。

入力と出力の両方が Observable でなければならないので、sockDriver(sink) のシンクが peer への外に行くメッセージの Observable でなければならないことを確認することはたやすいことです。逆に、ソースはやってくるメッセージの Observable でなければなりません。次のコードはドライバー関数のドラフトです。

function sockDriver(outgoing$) {
  outgoing$.subscribe(outgoing => sock.send(outgoing));
  return Rx.Observable.fromCallback(sock.onReceive)();
}

outgoing$ への購読は send() の副作用を実行し、sock.onReceive にもとづいて返される Observable は外の世界からデータをとります。しかしながら、sockDriver は sock がクロージャであることを想定します。見たように、sock はコンストラクターの新しい Sock() でつくられる必要があります。この以前関係を解決するために、sockDriver() 関数をつくるファクトリーを生成する必要があります。

function makeSockDriver(peerId) {
  let sock = new Sock(peerId);
  return function sockDriver(outgoing$) {
    outgoing$.subscribe(outgoing => sock.send(outgoing));
    return Rx.Observable.fromCallback(sock.onReceive)();
  };
}

makeSockDriver(peerId) は sock インスタンスをつくり、sockDriver() 関数を返します。これを Cycle.js アプリで次のように使います.

function main({sock}) {
  const incoming$ = sock;
  // outgoing$ (文字列メッセージの Observable) をつくる
  // ...
  return {
    sock: outgoing$
  }
}

Cycle.run(main, {
  'sock': makeSockDriver('B23A79D5-some-unique-id-F2930')
});

makeSockDriver(peerId) でドライバーがつくられたときに指定された peerId があることにご注目してください。main() が何かのロジックにしたがって異なる peer に動的に接続する必要がある場合、この API をもはや使うべきではありません。代わりに、「peerId に接続する」、もしくは「peerId にメッセージを送信する」など、入力としてのインストラクションをとるドライバー関数を必要とします。これはドライバー API を設計しているときに考慮すべきことの1つです。

ドライバーは Cycle.js を拡張性あるものにする

Cycle Core はとても小さなフレームワークであり、Cycle DOM の Driver はアプリのオプションプラグインとして利用可能です。このことは DOM Driver をユーザーとのインタラクトを提供するほかのドライバー関数に置き換えることはシンプルであることを意味します。

たとえば、DOM Driver をフォークし、プリファレンスに採用し、Cycle.js アプリで使うことができます。ソケットとのインターフェイスをもつドライバーをつくることができます。ネットワークリクエストを実行するためのドライバー。Node.js のためのドライバー。<canvas> もしくはネイティブなモバイル UI などのほかの UI ツリーをターゲットにするドライバー。

フレームワークとして、最近の Web 開発を支配するモノリスと比較することはできません。Cycle.js 自身は結局のところ RxJS Observable を使った外の世界とのリアクティブなダイアログをつくるための小さなツールと慣習だからです。

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8