-
Notifications
You must be signed in to change notification settings - Fork 0
Flow Development Guideline
フロー開発ガイドラインは、ソリューション開発やテンプレート開発における、Node-REDのフローの開発指針を示す。 一般的なプログラミングにおける開発指針と同様に、アーキテクチャ検討・設計、コーディングスタイル等の開発標準を用意すると、開発効率は向上する。 そのため、本ガイドラインは、PoC開発から、テンプレート開発に至るまでの流れに即し、上記観点に関するガイドラインを提供する。
フロー開発ガイドラインが特に強調するのは、可読性と再利用性の高いフローを実装することである。 それは、もしフローの可視性が低く処理内容の理解が困難だと、再利用するモチベーションは失われ、ソリューションコアの創生サイクルが円滑化されないためである。 そこで、本ガイドラインでは、より効率的に可視性と再利用性が高いフローを開発するための、Node-REDが推奨する開発指針を重点的に説明する。 また、Node-REDでのフロー開発前提として、Node-REDやNode-REDの基本知識やTips、Node-REDのプラットフォーム環境を踏まえた制約事項なども、適宜取り上げる。
Node-REDのアーキテクチャにおいて、フロー開発ガイドラインの内容に関係するコンポーネントは、以下の通りである。
- フローエディタ
- APIゲートウェイ
なお、本ガイドラインでは、Node-REDのフローエディタ内で用意されたノードのみを用いて、フローを開発することを想定している。 もし、独自にノードを開発する必要があれば、ノード開発ガイドラインを参照して頂きたい。
Node-REDには、主に以下のユーザーが存在する。
- フロントエンジニア
- ソリューション開発者
フロントエンジニアは、客先でソリューションの説明やデモをする。 フロントエンジニアは、本ドキュメントを全て読む必要はないが、Node-REDのフローは顧客とのコミュニケーションツールとして利用できるため、フローの作り方の概要はご理解頂きたい。 その理解に、本ガイドラインを活用して頂きたい。
ソリューション開発者は、Node-REDを利用して、PoCやソリューションを開発する。 Node-REDを利用し、テンプレートを組み合わせて再利用した迅速な開発を進めるにあたり、本ガイドラインを活用して頂きたい。
読者の職種や立場によって、Node-REDを利用する目的や、Node-REDで行う作業が異なるため、本ガイドライン内で読むべき範囲も異なる。 本ガイドラインは、以下の3種類の対象読者を対象にしている。
対象読者の種類 | Node-REDで行う作業 | 本ガイドラインで得られる効果 |
---|---|---|
フロントエンジニア | ・テンプレートの試用 ・部分的なフローの修正 |
・フローの理解速度向上 |
ソリューション開発者 | ・テンプレートの試用 ・フローの開発 ・テンプレートの再利用 |
・フローの理解速度向上 ・フローの開発速度向上 |
なお本ガイドラインでは、読者が事前に日立Node-REDエバンジェリスト執筆の書籍(※)による学習や、教育を完了し、以下の知識を有することを前提にしている。
- Node-REDに関する基本的な知識
- Node-REDを用いた基礎的なフロー開発の知識
(※) つないで つないで プログラミング Node-REDでつくる初めてのアプリ (外部サイト)
フロー開発ガイドラインが前提とする環境は、Node-REDで提供される環境とする。 なお、フローエディタを利用する際のブラウザは、Google ChromeまたはFirefoxを推奨する。
このマニュアルで使用している略語・用語の一覧を次に示す。
略語・用語 | 意味 |
---|---|
API | Application Programming Interface |
JSON | JavaScript Object Notation |
Node-REDでは、一つのアプリケーションを、"小さい処理"を組み合わせて構築する。 例えば、ニュース記事をメールで送信するアプリケーションを構築するためには、 「ニュースサイトからデータを取得」「特定の企業で絞込」「メール本文作成」「メール送信」といった "小さい処理"を逐次的に実行するように設計する。 Node-REDでは、この"小さい処理"を、 ノード と呼ぶ。
フローエディタでは、ノードは一つの処理の固まりとして表現されており、 ノード同士を ワイヤー と呼ばれる線で繋ぐことで、ノードの逐次処理の順序が記述できる。 一つのアプリケーションを実現するノードとワイヤーの繋がりを、 フロー と呼ぶ。
各ノードの処理は、ノードに メッセージ と呼ばれるデータが入力されることで開始される。 ノードの処理が終了すると、ノードはメッセージを出力する。 このメッセージの実体は、JavaScriptのオブジェクトだ。
フローエディタにおいて、メッセージはノードの左側に隣接したワイヤーから入力され、ノードの右側に隣接するワイヤーから出力される。 なお、ノードとワイヤーの接地点を 端子 と呼ぶ。 出力されたメッセージは、ノードの右側からワイヤーに流れ、ワイヤーに隣接する次のノードに入力される。 このように、フローで設計されたアプリケーションは、フローにメッセージが流れ、各ノードが逐次的に処理されることで実行される。 なお、フローの先頭のノードは入力端子がなく、そのノード自身がメッセージを生成して出力端子からメッセージを送出する。 また、フローの末尾のノードは出力端子がなく、このノードの処理が終了すると、フロー全体の処理が終了する。
多くのノードは、メッセージに含まれる値を読み取り、値に応じた処理を行う。 また、一部のノードは、メッセージに含まれる値を変更する。 そのため、フローの中で各ワイヤーごとに、流れるメッセージの内容は異なるだろう。
Node-REDの実行エンジンは、一般的なWebサーバーやアプリケーションサーバーなどと同様に常時起動している。 作成されたフローは実行エンジンに設置されると、フローの先頭のノードを実行する契機の待ち受け状態となる。 フローを実行エンジンに設置する作業や処理を、デプロイ と呼び、フローの先頭のノードが処理を開始する契機を、イベント と呼ぶ。 また、フローの先頭のノードを実行することを、単にフローを実行すると表現することもある。
より詳細なNode-REDの基本事項については、Node-REDの公式ドキュメントを参考されたい。
Node-REDを起動すると、ブラウザからアクセスできる。
ユーザーは、 ワークスペース と呼ばれるエリアに、ノードやワイヤーを配置してフローを作る。 ノードは、左側の パレット と呼ばれるエリアに並んでおり、一つづつドラッグアンドドロップでワークスペースに配置できる。 ワイヤーは、ノードの端子から別のノードの端子にドラッグアンドドロップで追加できる。
ワークスペースに設置されたノードは、クリックしてからCtrl+cでコピーでき、Ctrl+vでマウスカーソルの位置に複製されたノードが出現する。 ワークスペース内の任意の場所をクリックしたら、複製されたノードが配置できる。 複数のノードを範囲選択することも可能で、複数のノードを同時にコピーすることもできる。
Node-REDでは、一つのイベントに対する処理を、一つのフローとして記述する。 フローエディタでは、一つのフロータブ内に複数のフローを記述できる。 関連するフローを一つのフロータブ内に記述することで、タブに含まれるフローの意味が理解しやすくなる。
フローに含まれるノードの処理は、フローの左端から右端にかけて、メッセージが流れるごとに順に実行される。 Node-REDで標準に用意されている多くのノードは、同期的に処理されるように見える。 ただし、functionノードや、独自にユーザーが開発して利用するノードでは、ノードがメッセージを送出した後も引き続き処理し続けるようにできる。
また、一つのフローは、同時に複数のメッセージを処理することができる。 すなわち、フローの先頭のノードが、フローの末端のノードが処理を完了する前に、次のメッセージを送出することが可能だ。 ただし、Node-REDでは、基本的にメッセージの順序は保証されない。 メッセージの順序を意識した処理を行う必要がある場合は、後述する"Message Sequence"を参考にしてほしい。
なお、フロータブ内では、一つのフロータブ内をスコープとした フローコンテキスト と呼ばれる大域変数を利用できる。 また、全てのフロータブ(すなわち全てのフロー)をスコープとした グローバルコンテキスト と呼ばれる大域変数も利用できる。 これらの大域変数の利用に関するノウハウは後述するが、基本的には、処理の可読性を低減させる要因になるため注意が必要だ。
メッセージはJavaScriptのオブジェクトであり、msg
というオブジェクト名がついている。
Node-RED自体は、メッセージが表すデータの内容などに特に制限されていないが、比較的小さいデータを表す場合が多い。
例えば、IoTアプリケーションで1件のセンサーデータ、トランザクション処理がある業務システムでは、処理の開始を指示する信号(リクエスト)などである。
メッセージの設計方針と実装に関しては、実装の章を参照頂きたい。
メッセージには、以下の2通りの使われ方がある。
- ノードが処理するデータ、またはそのデータの参照情報 (すなわちノードの処理の入力データ)
- ノードの機能を制御するパラメータ
1点目の目的で使われるmsg
は、そのメッセージを利用するノードが、msg
オブジェクト内のどのパラメータに必要とする値が入っているか、分かっていなければならない。
つまり、msg
を生成するノードと利用するノードの間で依存関係が発生する。
この依存関係を緩和する方法も、実装の章を参照頂きたい。
なお、大容量のデータを処理する場合は、msg
オブジェクト内にデータを入れず、DBやファイルに格納してmsg
オブジェクトにはその参照情報を保持するのが、メモリ消費量を抑えられるので良い。
2点目の目的で使われるmsg
は、ノードに到着したメッセージ内の特定のプロパティに応じて、処理内容を変えるノードによって使われる。
例えば、Mqtt送信ノードは、メッセージ内にmsg.topic
プロパティが存在していたら、その値を参照し、MQTTメッセージのトピック名に利用する。
これは、ノードの仕様として定まっているため、msg
オブジェクト内のどのパラメータが利用されるかが、分かりやすい。
Node-REDに限らず、ビジュアルプログラミング全般に言えることとして、処理が見やすく、小さなアプリケーションであれば、ほぼプログラミングスキルなく、簡単に作成することができる。 しかし、アプリケーションの規模が大きいと、一般的なプログラミングでいう"スパゲッティーコード"が可視化されてしまう。 だが、この問題は、ある一定の注意を工夫を開発時に心がけることで、ある程度解決できる。 スキルに関係なく誰にでも可能であり、効果の大きい工夫として、まずはここから説明したい。
まず、一般的なプログラミング言語で、タブやスペースなどにより、処理の構造が見やすくなるのと同様に、Node-REDではノードはきれいに整列されるのが望ましい。 特に、ノードの並び方を工夫することで、処理の内容に対する何らかの意図を含ませられる。
Node-REDには、ノードをきれいに配列することを補助する『グリッド機能』が存在する。 これを有効化するだけで、最低限の統一感を持ったノードの配置が、自動的に実現できる。 有効化する方法は、Node-REDの画面の右上にある設定ボタンから、「設定」->「グリッドを表示」とクリックしていき、「ノードの配置を補助」にチェックをつけるだけだ。 設定が反映されると、エディター内にマス目が現れる。 そして、エディター内にノードを配置すると、ノードがマス目に合わせて配置されるようになる。
また、フローの並び方を整理するだけでも、タブに含まれる処理の内容を理解しやすくなる。 下の図では、1つのタブの中に、複数の処理を記述している。 複数の処理をまとめた処理群に対するコメントノードを追加し、各フローに対する説明と、そのフローを、一段階右にずらして配置している。 この配置により、フローの閲覧者は、下図には2種類の処理群が存在し、各処理群内の処理を個別に理解することが容易になる。
最も可読性の良いフローの記述方法は、一つの処理を、できる限り横一直線に並べることである。 フローの分岐がある場合は、分岐後のフローを縦にそろえて並べると、前段のノードから分岐された2つの処理を対比させやすい。
基本的には推奨しないが、ノードを縦に並べることが有効なケースもある。 下図には、ノードが縦に整列されている部分がある。 この縦に並んだノードの集合は、フロー内で意味を持った一まとまりの処理であるように見える(実際にそうである)。 一つの大きな処理があったときに、その中に含まれる小さな処理のまとまりと、その関連を捉えることができれば、大きな処理の概要を理解することは容易になる。 そのため、下図のように、一つのフローが長く、フローの中に処理のまとまりがある場合には、そのまとまりを構成するノードを縦に並べることで、フローの処理の概要が理解しやすくなる。
ノードやタブに適切な名前を付けることも、フローの可読性や再利用性に大きく影響を与える。 以下の命名規則は一例だが、命名規則の策定に迷った時には参考にしてほしい。
ノードには個別にノード名を付けると、ワークスペース上でそのノード名が表示される。 そのため、ノードの処理内容を端的に表現した名前を付けることで、フローの可読性が高まる。
ノードは、小さな処理単位だが、アプリケーションの中での各ノードの役割は様々だ。 例えば、アプリケーションにおける業務レベルの処理を行う処理規模の大きなノードもあれば、単なるデータ処理を行うノードもある。
これらのノードの役割や立ち位置に応じて、命名の仕方を変える方が、フロー全体の処理の見通しがよくなる。 そこで、以下のような命名規則を提案する。
- 高レベル(業務レベル)のフローに含まれるノード
- 業務処理が分かりやすいノード名
- 低レベル(処理実装レベル)のフローに含まれるノード
- 「動詞+目的語」「動詞+目的語+副詞」
このように、処理のレベルで命名規則を分ける理由は、フローを閲覧・利用するユーザーごとに関心が異なるためだ。 高レベルのフローは、アプリケーションの概要を簡潔に知りたいユーザーにとって重要なノードだ。 低レベルのフローは、エンジニアも閲覧・修正を行う。 そのため、高レベルのフローでは、業務処理が分かりやすいように、ノードに名称を付けるべきであり、 低レベルのフローでは、処理内容が把握しやすいように、ノードに名称を付けるべきだろう。
高レベルのフローに含まれるノードには、その名称のみで業務内容がイメージできる名前を付ける。 高レベルのフローは、大規模になりにくいため、名前は多少長くてもよく、目安として、50文字以内を推奨する。 ただし、業務内容の補足情報などは、ノードの情報タブに記載するようにする。
低レベルのフローに含まれるノードには、処理内容がイメージできる、端的で統一感のある名前を付ける。 一般的なコーディングルールとして、関数名を「動詞+目的語」や「動詞+目的語+副詞」で命名する場合が多いため、 「動詞+目的語」または「動詞+目的語+副詞」で統一することを推奨する。 もしこれ以外の品詞を用いる場合でも、品詞の順序は統一することが望ましい。
タブにも名前を付けることができる。 タブの命名規則には、以下を提案する。
- 「名詞」
- 複数単語からなる名詞でも構わない
- 20字以内に収める
タブは、複数のフローをアプリケーションとしての意味のある単位にまとめるものである。 そのため、タブの名前は、機能および一連の業務を端的に指し示す名称がふさわしい。 よって、フローの名前を「名詞」で統一することを推奨する。
フローに付ける名前は、複数の単語から構成される名詞でも構わない。 ただし、ブラウザ(を表示するディスプレイ)のサイズによっては、20字以上の名前を付けると、20文字目以降が読み取りにくくなる。 そのため、20文字以内で命名することを推奨する。
ノードの種類には、それぞれ期待される役割がある。 例えば、changeノードであれば、何かの変数の値を変更する処理が行われることが期待される。
しかし、ある処理が、複数の異なるノードで実現できるケースがある。 例えば、changeノードが行う値の生成は、templateノードで代用できる。 splitノードによるメッセージ分割は、changeノードのJSONata構文を用いることでも実現できる。 しかし、各ノードを、本来と異なる用法で用いると、他者がフローを見た時に理解しにくく、さらに誤認識する危険性もある。 そのため、ノードは、ノードの本来の役目通りに利用するべきである。
参考までに、下記に、多くのユースケースで頻繁に利用されるノードの本来の用法を記載する。
ノード名 | 主な用途 |
---|---|
changeノード | メッセージやコンテキスト内のプロパティの生成や、値の変更・削除・移動 |
switchノード | メッセージのプロパティやコンテキストの値に応じたフローの条件分岐 |
templateノード | 入力されたデータとあらかじめ用意した定型文を組み合わせたデータの生成 |
linkノード(in/out) | (in/out)フロー間を仮想的なワイヤーで連結 |
http in/http responseノード | (http in) HTTPエンドポイントの設定、(http response)HTTPリクエストの送信 |
Node-REDには、任意の処理を実行できる"万能ノード"が存在する。 これらのノードは、便利である反面、フローの可読性の観点では注意が必要である。
- functionノード
- execノード
functionノードでは、ユーザーがfunctionノードの設定画面上にJavaScriptのコードを記述することで、任意の処理を作成することができる。 functionノードを用いることで、Node-REDの制約の範囲内で、JavaScriptで記述可能なあらゆる処理が実現できる。
JavaScriptに慣れているユーザーは、全ての処理をfunctionノードで記述したくなるかもしれない。 しかし、functionノードの多用を非推奨とする。 その理由は、フローを見ただけでは、フローの処理内容が把握しにくくなるためである。 また、JavaScriptに慣れていないユーザーもNode-REDを利用するが、functionノードが多用されているフローは、そのようなユーザーにとって再利用しにくいものである。 さらに、functionノード内でグローバルコンテキストやフローコンテキストなどを用いると、コンテキストに依存するノードでバグが存在する場合に、バグの特定に時間を要する。
不必要なfunctionノードの典型例は、変数への値代入や変更を、functionノード内で記述しているケースである。 変数がドメイン内の業務において重要な意味を持つ場合は特に、functionノード内に値代入・変更の処理を記述せず、changeノードを利用し、変数に値を代入したことを他のフロー閲覧者に明示することが重要である。 changeノードは複数の値代入を1ノードで記載することも可能であり、さらにJSONata構文によるJSON操作用のスクリプトを記述することも可能なので、functionノードを利用せずに、柔軟な値代入・変更が実現できる。 詳細は、本ガイドラインのAppendixや、Node-REDのchangeノードの仕様を参照して頂きたい。
ただし、ドメインロジックとして大きな意味を持たない処理を、functionノード以外で記述すると、フローが大規模化する時もある。 特に可読性を求める必要のない一連の処理によって、フロー全体の可読性が低下するのは本末転倒である。 この場合には、functionノードを利用する方が、可読性が向上する。
また、たとえドメインロジックとして大きな意味を持たない処理であっても、頻繁に変更されうる処理は、functionノード以外で記述する方が望ましい。 例えば、他のフロー開発者が、フローの再利用時に変更を加えることが予想される処理や、業務内容の更新などによって、継続的に編集された処理などである。 あらかじめ修正されることが分かっているのであれば、修正ポイントがフローから明確化されるように、functionノードではなく、処理の流れが捉えやすいノードで記述するのが良い。
そのため、functionノードを利用する際の指針としては、【他者が見る必要のないほどドメイン内で重要度が低く】かつ【変更機会が少ない】処理は、functionノードに記載してよいだろう。
メリット | デメリット |
---|---|
・ノード/フローにとらわれず、フレキシブルに処理がかける ・フローの規模が縮小できる |
・フローを見ただけでは処理内容が分からない ・JavaScriptに慣れていない人には分からない |
execノードは、Node-REDが稼動するLinuxサーバーのコマンドを実行できるノードである。
execノードを用いることで、Linuxサーバーの中に任意のプログラムを用意し、execノードでそのプログラムを実行すれば、任意の処理を実現することができる。 これにより、フロー開発者は自身の好きなプログラミング言語で処理を実装でき、テストすることが可能になる。
しかし、このようなexecノードの利用方法を推奨しない。 その理由は、functionノードの時と同じく、他者からはフローを見ただけでは処理内容が理解できないためである。 また、フローを再利用するためには、開発者が用意したプログラムと、特定のLinuxサーバーの環境が必要となるため、作成したフローの配布が困難である。
execノードの利用が適しているのは、以下のケースのみである。
- 実行基盤(サーバー)へのファイルの保存または取得
- 実行基盤に依存した処理の実行
例えば、一時的にログ等のデータをサーバー内に保存したり、削除する場合には、execノードでLinuxコマンドを実行する必要がある。 ただし、rmコマンドや通信系コマンドなどを使う場合、Node-REDの環境の破壊やセキュリティ面での危険性がある。 それを十分理解した上で、execノードは必要最小限に用いるのが良い。
メリット | デメリット |
---|---|
・実行基盤(サーバー)依存の処理を実行できる ・任意のプログラムを実行できる |
・フローを見ただけでは処理内容が分からない ・rmや通信系コマンドなどを使う場合、環境破壊やセキュリティ面で危険性が高い |
大量のデバイスを複数のMQTTノードで管理するケースなど、同じ種類のノードがフロー内で混在すると見分けが付きにくくなる場合がある。このような場合には、ノードごとに異なるアイコンを指定することができるアイコンの変更機能が役立つ。 注意点として、アイコンの変更機能を多様し過ぎると、ノードの視認性が下がる可能性もあるため、バランスを考慮して利用すること。
アイコンの変更は、以下のようにノードの編集画面の設定タブで行うことができる。 選択ボックスの左側はアイコンファイルを含んでいるNode-REDノードのパッケージ一覧を表示し、右側で選択したパッケージ内に含まれるアイコン一覧を表示する。
自身で作成した画像ファイルをノードのアイコンに指定することも可能である。
$HOME/.node-red/lib/icons
にpngファイルを置いてNode-REDを再起動することで、Node-REDノードのパッケージ一覧にLibrary
という項目が増え、そこから独自のアイコンを選択できるようになる。
以下、注意事項について述べる。
- 画像の要件については、Custom iconを参照すること。
- 独自のアイコンを利用する場合、フローを他のマシン上にインポートした時に、インポート先でもアイコンのアップロードが必要となる。
- Node-RED v0.20以降は、font-awesomeのアイコンに対応する予定で、対応した後は様々なアイコンを利用できるようになる。
主なユースケースとしては、以下を想定している。
- 大量のデバイスを管理する際に、MQTTノードにデバイスのイメージを貼り付ける。
- functionノードまたはサブフローノードで、内容を表すイメージを貼り付ける。
- コメントノードで、重要度に応じて色分けする。
小さいフローを開発する際は、アドホックに開発を進めても問題ないが、ロジックの複雑な大きめのフローを開発する場合は、戦略的に設計・開発を進めていくのが良いだろう。
大きく、開発の流れを、設計フェーズ、開発フェーズ、改善フェーズと分けた。 本ページでは、設計フェーズについて説明する。
設計フェーズでは、まず、アプリケーションが対象とする処理を、整理しよう。 対象とするドメインには、どのようなモノ、人、作業、ルールなどがあるだろうか。 そして、それらがどのような関係性を持つかを、ノートがホワイトボードで考えてみてほしい。 また、アプリケーションの目的と、使われ方も定義しておく。
ドメインについて整理ができたら、そのドメインに含まれる処理をまとめていく。 この作業の意図は、実装フェーズにおいて1つのタブに実装する処理内容・規模を決めることだ。
補足だが、基本的にフローベースプログラミングは、状態の管理を得意としない。 なぜなら、処理の流れを可視化するのがフローベースプログラミングの良いところであるが、状態はフローとして静的に可視化され得ないためだ。 それでも状態の管理やトランザクションを必要とするアプリケーションは存在するため、上記の説明はその場合の対応として捉えてほしい。
ドメインモデルの設計を行ったら、それを反映するようなフローの構造を考える。
タブ内では、一つの意味のある業務処理を行う。
また、タブの中では、後述するmsg.payload
の扱いや、フローコンテキストなどを、そのタブに閉じたルールで利用できる。
そのため、トランザクションが存在しうる場合は、出来るだけ一つのタブの中で、トランザクションが完結するのが良いだろう。
実装の複雑さが、緩和される。
また、一つのアプリケーションを成立させるためには、往々にして複数のタブが連携することになるため、タブ間のI/Fを検討する。
タブに入ってくるメッセージとして期待するmsg
オブジェクトを定義し、タブのコメントとして記載しておくのが良い。
フローに流れるメッセージは、JavaScriptのmsg
という名前のオブジェクトだ。
このメッセージは、部分的にNode-REDやNode.jsの制約や実行モデルに影響を受けるが、メッセージオブジェクト内に任意の属性を持たせられ、任意の型の値を入力することが出来るため、自由度が高い。
すなわち、msg
オブジェクト内のプロパティは、全て修正可能であり、新しいプロパティを付けて良い。
ただし、メッセージは、そのメッセージを生成するノードと、そのメッセージを利用するノードの間で依存関係が生じる原因である。
例えば、もしあるノードが、入力メッセージのmsg.payload
に温度の情報が含まれていることを期待しているのであれば、そのノードの前段のノードでは、msg.payload
に温度の情報を入れないといけない。
大規模なフローを記述したら、メッセージは多数のノードを辿るため、このような依存関係を明確にしないと、フローの再利用性や変更への柔軟性が著しく損なわれる。
そこで、フローの構造と共にメッセージについても、フローの再利用性や柔軟性を高くするように、適切な設計を検討すべきだ。
また、メッセージの設計によって、システム設計が変わることもあり得る。 フローの実装に入る前に、メッセージの設計を行っておく必要がある。
msg
オブジェクトのプロパティ名に命名制約はないため、意図せずに複数のノードが同一のプロパティを使用する危険性がある。
この危険性を抑えるため、設計上のルールとして、データの位置づけごとに、msg
オブジェクト内でそのデータを格納するパラメーターを決めておいた方が良い。
メッセージの説明で、メッセージには、ノードが処理するデータとノードの機能を制御するパラメータの2種類があることを書いた。
前者のデータは、msg.payload
プロパティ内に設定することを推奨する。
なぜなら、後者のデータは、Node-REDが提供するノードがmsg.
直下のプロパティを利用するためだ。
例えば、MQTT出力ノードでは、到着したメッセージのtopic
プロパティ(msg.topic
)を参照し、MQTTブローカーに送信するメッセージのトピック名を設定する。
下の図は、functionノードproc1で、msg.topic
プロパティを、ノードの処理対象のデータの格納先として用いて、バグが生じた例である。
MQTT送信ノードではトピック名を"Building/Room1/temperature"に設定しているものの、トピック名が意図しない値("hot"または"cool")となり送信される。
特に注意したいのは、MQTTノードのように、ノードの機能の制御を、サイドバーに表示されるノードの設定に記述することも、msg
オブジェクト内にも設定できるノードだ。
そのようなノードでは、例えサイドバーで設定を記載していても、msg
オブジェクトの設定の方が優先される。
だからこそ、これら二つの情報は、msg
オブジェクト内で、格納すべきパラメータを明確に分けるべきだ。
このポリシーに基づいて設計をしても、ノードの処理対象のデータは、多数存在しうるので、それらの間でプロパティ名の衝突が発生しうる。 ノード間でのプロパティの依存関係を機械的に確認する方法が存在しないため、ユーザーは各ノードがアクセスするプロパティを把握せねばならない。
この問題に対応するためには、依存関係にある複数のノードが、タブを超えて存在しないように、フローの構造を設計する。 そして、できる限り、タブの中のフローをできるだけ小規模にする。 これだけで、だいぶ依存関係を把握しやすくなる。
さらに、タブ内のフローの先頭のノードが、入力として与えられたメッセージが、期待するプロパティ構造であるかを検証する。 存在すると期待するプロパティ以外は、そのタブの中で自由に使ってよいものとする。 この検証を行う処理が、タブ間での依存性の境界の表現にもなる。
Node-REDでは、ノードを自作して、Node-REDの画面上で利用することができる。
その際、作成したノードの機能を、msg
の中のパラメータで制御する場合がある。
方針として、独自ノードの機能を制御するパラメータは、msg.[ノード名]_[パラメータ名]
で設定するのが良い。
msg.
直下のパラメータを利用する理由は、フローの処理対象のデータは、上の節で説明した通り、msg.payload
プロパティ以下で設定するためだ。
また、msg.
直下のパラメータは、Node-REDの標準ノードも利用するため、それらと衝突しないように、[ノード名]_[パラメータ名]
とする。
msg
オブジェクトが冗長になるが、msg.[ノード名].[パラメータ名]
とするのも悪くはない。
ユーザーが独自ノードを作成する時は、ノードの機能を制御するパラメータが、msg.[ノード名]_[パラメータ名]
で入力されるものとして、ノードを開発する。
注意して頂きたいのは、ノードの機能を制御するパラメータのプロパティ名は、フローの設計と独自ノードの間のインターフェースとなるため、後方互換が必須である。 そのため、極力プロパティ名の付け方に関して後方互換のない変更はしない。
Node-REDのノードは複数の出力端子を持つことができるが、入力端子は1つしか持つことができない。フロー上を流れるメッセージにはノードの制御を表すためのメッセージと処理対象のデータを表すためのメッセージが存在する。このため、フローの実行パス毎に別種の入力メッセージを区別する必要がある場合には、メッセージ内のプロパティによって区別を行う必要がある。
具体的には、実行パスを区別するタグを、メッセージのプロパティに付与し、入力パス毎に異なるタグ値を設定する。ノード処理では、タグ値を確認することでメッセージに対する処理を選択する。タグの設定にはtemplateノードを利用できる。
これにより、複数種のメッセージを入力するノードにおいて、メッセージ種別に応じた処理が可能になる。
ノードの処理対象のデータをmsg.payload
内に格納することは、上記したが、大量のデータを扱い場合は、注意が必要だ。
メッセージはメモリ上に確保されるため、大量のデータをメッセージに含めると処理時間、使用メモリ量に大きな負荷を与えてしまう。
特に、メモリが枯渇した場合にNode-REDの処理全体が停止してしまうことに留意する必要がある。
このような問題を避けるためには、データの実体をNode-RED外の永続記憶に格納し、メッセージにはその参照を保持する方法が考えられる。 フロー上のノードは参照を受け取り、永続記憶上のデータに対する処理を行うよう構成する。
フロー上ではデータの参照しか受け渡されないので、データ実体を期待するノードを利用できない。
特に、Node-REDが標準で用意しているノードや、他者が別の用途で作成していたノード・フローの再利用ができない可能性がある。
参照先のデータを、細かく分割してからmsg.payload
に入るようにNode-REDにデータを投入する、などの工夫が必要だ。
こういったトレードオフを考慮して、本ガイドラインの適用を検討する必要がある。
また、ノードが期待する入力メッセージの検証(validation)が可能になる。 期待していないメッセージが届いた場合のエラーハンドリング処理などを追加することで、デバッグも容易になるだろう。 エラーハンドリングについては、信頼性・品質向上で説明する。
複数のメッセージを受け取る処理でメッセージの到着順は保証されない。 そのため、到着順に依存した処理を行わないように、フローの処理を設計する必要がある。 つまり、メッセージの到着順序が想定と異なった場合でも、正しい結果が得られるようにするべきだ。
これについて解説するために、Node-RED自体の説明をする。 Node-REDは、非同期型イベント駆動型のプログラミング言語であるNode.jsで開発されている。 それにより、Node-REDでは、各ノードの処理がイベント駆動で実行される。 ここでのイベントとは、ノードにメッセージが入ることである。
Node-REDで各ノードが非同期にイベント駆動で実行されるということは、フローに複数のメッセージが流れている時に、それらのメッセージ間で処理の同期がとられない、ということだ。 同期を取らないことで、あるメッセージに対する処理が、他のメッセージに対する処理の終了を待つ必要がないため、フローは、より多くのメッセージを同時に処理できる。 その代り、先にフローに入ったメッセージが、先に処理される、といった処理順序の保証はない。
もし、フローに投入されたメッセージの順序に依存した処理を行う必要がある場合は、フローの中に処理順序を管理する処理を、ユーザー自身で実装する必要がある。 例えば、一度受け取ったメッセージの情報をノードコンテキストなどに蓄積しておき、必要なメッセージが揃った段階で処理を行うと言った方法を検討する。
// メッセージの蓄積処理
var msgs = context.get('messages') || [];
msgs.push(msg);
if(msgs.length === ...) {
... // 複数メッセージの処理
}
context.set('messages', msgs);
概要
- フローの先頭と末尾のノードをhttp inノード、http responseノードにし、フローをサービス化する
メリット
- フローの再利用性を高める
- 健全性監視システムと連携し、フローにおける処理の異常を検知することができる
留意点
- エンドポイントとフロー内部の処理内容は、後方互換を保つ必要がある
- 後方互換を保てなくなった場合に、開発者に対し非推奨になった・廃止されたことを伝える手段を検討する
- HTTPのエンドポイント名は分かりやすい名前にする
- フローの末尾にhttp response出力ノードを設置し忘れないよう注意する
- HTTPリクエスト元のノードがResponse待ちで停止する可能性があるため
解説 一つのサービスが、複数のサービスの連携で構築される場合がある。 例えば、ある設備に取り付けられたセンサーデータから設備の故障予兆検知を行うサービスの場合では、 以下のサービスを連携し、目的のサービスを構築する。
そこで、フローを他のフローから利用しやすくするために、 フローの処理をHTTPリクエストで受けることで開始できるようにし、 フローの処理が終了したら、リクエスト元のフローにレスポンスを返すようにする。
そのために、フローの先頭にhttp inノードを設置し、HTTPのエンドポイントを用意する。 また、フローの末尾にhttp responseノードを設置する。
なお、タブ間のフローのI/Fに相当する部分は、linkノードを用意する方法もある。 linkノードとhttp in/http responseノードには、メリットとデメリットがあるので、状況に合わせて使い分けてほしい。
メリット | デメリット | |
---|---|---|
linkノード | linkノードをクリックすることで、フロー間の連携を参照できる。 | タブ内にlinkノードの対があると、タブのI/Fとして機能するlinkノードと、タブ内のフローの簡略化のためのlinkノードの見分けがつきにくい。 |
http in/http responseノード | テスト用のシステムなど、Node-RED以外と連携できる。 | 不要に外部に処理を公開することになる。 |
概要
- 入力メッセージを加工して出力するタイプのノード処理では、変更されないプロパティが出力メッセージ中にそのまま保持されるようにする
メリット
- 複数のノードでメッセージプロパティでデータのやり取りを行っている場合に、間に位置するノードで必要なデータが失われることを抑止する
留意点
- (特になし)
解説 前出のように複数のノード間で、メッセージのプロパティを利用してデータを共有する場合がある。 メッセージ処理において、送出するメッセージは新規に生成することも可能であるが、入力メッセージに含まれるプロパティが失われ不具合が生じる可能性がある。このような問題が発生しないよう、入力メッセージ中のプロパティを出力メッセージにも保持するようにすべきである。
// OK: プロパティを保存
this.on('input', function(msg) {
msg.payload = 'some value';
node.send(msg);
})
// NG: プロパティが保存されない
this.on('input', function(msg) {
msg = { payload : 'some value' };
node.send(msg);
})
フローで作るアプリケーションの環境情報や設定情報などを、環境変数として記録し、任意のフローから参照するケースが存在する。 例えば、フローを実行するOSの種類や、連携する外部サービスのアクセス情報などをフローの環境変数として記憶し、フローの随所でその変数の値に則した処理に切り替えることなどがある。
これに対し、global
コンテキストは、任意のフローからアクセス可能な変数である。
しかし、「状態管理」で後述するように、global
コンテキストを様々なフローから利用すると、global
コンテキストへの依存箇所が特定しにくくなり、バグが発生しやすくなる。
そこで、フロー開発においては、以下のような取り決めを行う必要がある。
- 環境変数に値を代入するのは、ある特定の一つのサブフロー内に限定する
- 1.を利用しない環境変数の値代入は、行わない
- 環境変数の設定は、フローの実行時にのみ行う
1.2.により、global
コンテキストの操作を行っている箇所が、フローから見やすくなる。
また、3.により、global
コンテキストとの依存性を極力減らす。
もし、global
コンテキストの値変更が、上記のルール以外で行われていないかを確認する必要がある場合は、上記のサブフロー内で、global
コンテキストの値が変更されるごとに、変更前と変更後の値をログに出力する。
これにより、このサブフロー以外の方法で値が変更された場合は、ログやそのログを用いた検知手段を用意することで判明できるようになる。
複数のメッセージを対象とした計算処理、複数ノード間の情報共有、Node-RED外部の状態に依存した処理、エラー発生時のリカバリ処理などではプログラムの実行状態を管理する必要がある。 本節ではこういった処理における状態管理の方針について説明する。
Node-REDにおいて状態管理に利用可能な仕組みは以下が存在する。
- コンテキスト
- Node-REDにおいて状態管理の仕組みを提供するオブジェクト
- 状態を参照可能な範囲によって、ノードコンテキスト、フローコンテキスト、グローバルコンテキストの3種のオブジェクトが存在する
- コンテキストブジェクトはデプロイ、もしくは、Node-REDの起動時に初期化される
- ノードコンテキスト
- ノード毎の状態を保持するオブジェクト。状態の参照はget、設定はsetメソッドで行う
// ノード(this)のノードコンテキストオブジェクトの参照
var contex = this.context;
var count = context.get('count') || 0;
count += 1;
context.set('count', count);
- フローコンテキスト
- 同一フロー内のすべてのノードから参照可能なオブジェクト
- 状態の参照はget、設定はsetメソッドで行う
- 同一フロー内のすべてのノードから参照可能なオブジェクト
var flowContext = this.context.flow;
var count = flowContext.get('count') || 0;
- グローバルコンテキスト
- すべてのノードから参照可能なオブジェクト
- 状態の参照はget、設定はsetメソッドで行う
- 値の更新箇所および参照箇所が、任意のフローにまたがり、フローの保守が困難になるため、利用は推奨しない
- ただし、ソリューションの環境変数を共有するためには有用である
- 環境変数の共有に用いる場合は、フローの中で値を更新している箇所を限定し、明確になるように気を付ける
- すべてのノードから参照可能なオブジェクト
var globalContext = this.context.global;
globalContext.set('foo', 'bar');
- メッセージのプロパティ
- メッセージ内のプロパティに状態を表す情報を格納してノード間で受け渡しを行う
- メッセージの受け渡し先でしか参照できないため、単一ノード内の状態保持のためには利用できない
count = msg.count || 0; // メッセージのプロパティを参照
msg.count = count +1;
node.send(msg);
- 大域変数
- Node.jsの大域変数に状態を保存
- ノード間の競合による不具合の原因となりやすいため、利用は推奨しない
count = global.count || 0; // 大域変数の参照
global.count = count +1;
- クロージャの束縛変数
- ノード定義時のクロージャの束縛変数に状態を保存
- この方法はNode-REDの標準ノード実装でも利用されているが、束縛変数により実現できる機能は基本的にノードコンテキストと同等であるため、ノードコンテキストの利用を推奨する
- なお、これらの情報はデフォルトではメモリ上のみに保持される
- 将来的には、Redisなどのキー・バリューストアへの永続的保存が計画されている
module.exports = function(RED) { // ノード定義
function ExampleNode(node) {
var count = 0; // 束縛変数
this.on('input', function(msg) {
count = count+1;
...
});
}
...
}
概要
- 複数のメッセージの処理で共通に使うデータの保持にはノードコンテキストを利用する
- 複数のノードでのデータ共有は、メッセージを介して行う
メリット
- フローとそのメッセージを見るだけでノード間のデータの流れが把握できる
留意点
- ノード間で共有するデータ量が多い場合、ノード間のメッセージ送受信オーバヘッドが大きくなる
解説 コンテキスト(ノード、フロー、グローバル)を用いると複数のメッセージ処理にまたがった情報共有(時間的共有)や複数のノード間での情報共有(空間的共有)を実現することができる。 時間的共有を用いることで、関連のある複数のメッセージに対する処理を実現することができる。 また、空間的共有を用いることで、離れた位置にあるノード間でのデータのやり取りを低いオーバヘッドで行うことができる。
情報管理を局所化する観点から、このようなコンテキストの利用は同一ノードでの時間的共有に限定することが望ましい。 空間的共有の利用はフローの接続から把握できるデータの流れと異なるデータの流れを作り出す可能性があるため、その使用は限定すべきである。 よって、複数のノード間でのデータ共有はメッセージを介して行い、フロー上の接続関係によってその関係を把握できるようにする。
なお、異なるフロー間の情報共有にメッセージを用いることが難しい場合は、読み出し/書き込みを行うノードを対として外部の永続記憶を用いて行うことを推奨する。
関連ガイドライン
概要
- コンテキスト上の情報を参照する際には、プロパティを直接参照するのではなく、get/setメソッドを解して行う
メリット
- コンテキストの永続記憶への退避など、今後の拡張への対応が容易となる
留意点
- (特になし)
解説 Node-REDのコンテキストオブジェクトは通常のJavaScriptオブジェクトのため、プロパティのアクセスを行うメソッドを使用しなくても、値の参照や設定を行うことができる。 将来のNode-REDでは、コンテキストデータの永続記憶への保存などが計画されており、これを利用する上でget/setメソッドを利用することが前提になるものと想定される。
var context = this.context;
// OK: get/setを利用
count = context.get('count') || 0;
context.set('count', count+1);
// NG: プロパティを直接参照
context = context.count || 0;
context.count = count +1;
関連ガイドライン
- (特になし)
Node-REDは高い計算可能性を有し、多様な処理を簡潔に記述することが出来る。 しかし、その柔軟さにより、無限ループに陥るフローや、永遠にエラー処理が終わらないフローなど、実行エンジンのリソースを枯渇させシステム全体に悪影響を及ぼしうるフローも記述可能である。 そこで、フロー設計において、一定の原則を設ける。
概要
- 無限ループに陥ることを防ぐために、フロー中にループを設けた場合は、必ずループからの脱出方法を用意する
留意点
- ループからの脱出のトリガーにコンテキストを用いる場合、脱出に用いるコンテキストを、 フロー内の別のノードからアクセスしないように気を付ける
解説 下の図のように、ワイヤーを8の字につなぐことで、ループを設定することができるが、 メッセージがループに流れ続け、無限ループに陥る。 無限ループに陥ったメッセージを止めるためには、実行エンジンを停止させなければならない。 この事態を防ぐために、必ずメッセージがループから脱出する手段を用意する必要がある。
ループから脱出する方法は複数考えられる。 例としてfor文を模したフローにより、ループから脱出する方法を提供している。
また、下のフローでは、フローコンテキストを用いることで、メッセージがループから脱出するタイミングを、ユーザーが指定できる。
「脱出(flow._loop=false)」ノードがフローコンテキストの_loopプロパティを参照し、
値がfalseの場合はループから脱出する。
メッセージがループに入った時点では値がtrueになっており、ユーザーが任意のタイミングで、
「ループ終了」ノードをクリックすることで「flow._loop:=false」ノードが処理され、flow._loop
の値がfalseに更新される。そのため、上のループからメッセージが脱出する。
関連ガイドライン
- (特になし)
他者から理解されやすいフローの作成に心がける必要がある。 その観点から、コメントは重要であり、開発したフローには必ず、各フローの役割や注意事項を適切に表現したコメントを残さなければならない。 そこで本節では、コメントの書き方についてガイドする。
Node-REDでは、以下の箇所にコメントを残すことが出来る。
- タブごとのコメント
- タブ内のフローへのコメント
- サブフローへのコメント
それぞれのコメントの書き方やコメントの意義が異なるため、以降では個別に説明する。
フローエディタのノードやワイヤーが存在しない場所をクリックすると、情報タブ内の情報欄に、そのタブのコメントが表示される。 このコメントはフロー開発者が自由に編集することが出来る。 編集方法は、まずタブをダブルクリックして下図の「詳細」欄を表示させ、次にMarkdown形式で説明を記載すると、情報タブ内の情報欄に内容が反映される。 なお、Markdownについては、Official Markdown Project(外部リンク)を参照されたい。
タブへのコメント内容は、そのタブに含まれるフロー全体の概要を、端的に説明する程度で良い。 フローの詳細は、コメントからではなく、フロー自体から読み取れるようにフローを設定しておくべきだからである。 また、Node-REDにはフロー開発者が容易にフローを修正できるメリットであるため、フローの詳細をタブのコメントとして残すと、コメントとフローの実装の対応の維持が難しく、かえって開発コストが増大する。 ただし、タブ内のフローが前提とする条件や、他のタブとの依存性、インターフェースが存在する場合は、他のフロー開発者への注意事項として、記載しておくのは良いだろう。
フローには、commentノードを用いて、フローの補足を記載することが出来る。 フローを操作し利用するユーザーが存在する場合には、commentノードでフローの説明を残すのが良い。
commentノードのノード名は、フローの処理概要を一言で表すものとし、一般的なプログラミングでの関数名やメソッド名に相当する語句とすると見やすい。
また、もしフローの入力msg
に前提がある場合は、その旨をcommentノードの「本文」に記載する。
これは、一般的なプログラミングでの、ドキュメンテーションコメントに相当する(参照:JavaDoc(Wikipedia), Python docstring)。
ただし、Node-REDの場合、コメントをむやみに多く残すことは推奨しない。 なぜなら、Node-REDのメリットは、フローを容易に修正しやすいことであるが、commentノードが多いと、フローの処理とコメント内容の対応を管理する開発コストが増大するためである。 よって、一つのフローに対して、一つから数個程度のcommentノードが妥当だと思われる。
サブフローは、一般的なプログラミング言語におけるライブラリに相当するフローである。 サブフローは他のフロー開発者によって積極的に再利用するため、後から利用されることを前提として、やや詳細にコメントを記述する必要がある。
以下に、サンプルフローのサブフローの説明を示す。
このフローには、7つのサブフローが存在し、それぞれについて、入力msg
と、出力msg
に関する説明が記載されている。
このように、サブフローには、サブフローを利用するために必要な情報として、入出力は必ず書く必要がある。
特に、サブフローの中で、値が変更・削除されるmsg
内の属性を明記しなければならない。
この明記がないと、フロー開発者がサブフローを再利用した際に、意図と異なるmsg
内の値変更が発生し、バグを引き起こす危険性がある。
そのため、サブフローを開発する際は、サブフローに入力されたメッセージが期待する属性が存在することをチェックするテストフローを用意するべきである。
また、出力時に設定・変更されている属性も、正しく処理されていることを確認できるテストフローも用意する。
そして、サブフローのコメントには、そのテスト内容(もしくはテストタイトル)に対応した入出力メッセージの情報を記載する。
さらに、サブフロー内で行われる処理についても、サブフローを利用する開発者にとって有用な情報であれば、積極的に記載するのが良い。
例えば、global
やflow
コンテキストの修正、処理時間が長い場合は予想される処理時間、発生しうるエラーとエラーmsg
の内容、などである。
ログは、フローの稼動状況や障害などの分析を行うために利用される。 これらの分析を効率的に行うためには、ロギングに関する以下の項目を、フロー内で統一させるべきである。
- フォーマット
- フロー内のログ出力箇所 (どういった処理に対してログを出力させるか)
- ログの出力先
ログには、分析に必要な情報を含める必要がある。 また、ログ内の文字列検索や文字列操作が簡単になるように、フォーマットを設定しておく。
ログには、主に以下の情報を含めると、稼動状況や障害の分析が行いやすくなる。
- 時刻
- ログレベル (または稼働状態)
- ログを出力したノード
- ログ出力時に流れた
msg
3.4.は、システムの稼動をトレースするために必要である。 これらは、開発したシステムの障害究明だけでなく、開発したシステムにおいて頻繁に利用される処理内容を捉えることにつながる。 これは、アプリケーションの利用者のニーズ分析や、それに伴うアプリケーションの改良、さらにはビジネスプロセスの改良のヒントにすることも出来る(参考:Process Mining)。
3.4.は、正常系のフローで取得する方法が存在しない(Node-RED 0.18現在)。 ただし、catchノードで捉えたエラーイベントには含まれる情報であるため、3.4.をログとして出力するためには、フローのログを記憶したい箇所で、functionノードで意図的にエラーを発生させる。
上記のような、ログに含める情報の決定は、そのフローの稼動において、ログをどのように利用するかに依存するため、本ガイドによって統一化することは好ましくない。 ログに含める情報は、作成するアプリケーションやテンプレートの内容や要件に従い、適切に取り決めてほしい。
ログの内容よりも重要なことは、一つのアプリケーションにおいて、ログのフォーマットが統一されていることである。 なぜなら、フォーマットが統一されていないと、ログの利用が困難になるためである。 しかしながら、ログを出力する箇所が、アプリケーション全体に点在していると、全ての箇所でフォーマットを統一することが難しい。 これを解決するためには、ログの出力を一つのサブフロー内で完結させるのが良い。
templateノードには、Mustache構文で以下のように記述されている。
{% raw %}[{{logid}}] "{{payload}}"{% endraw %}
このサブフローを利用するフローは、catchノードの後に、msg.payload
、msg.logid
、msg.level
を設定の上で、このサブフローにmsg
を送出する。
以下のようなログが、Node-REDを起動させたサーバーのコンソールに出力される。
[error] [function:ERRORログ出力] [E0001] "HTTP ERROR (404)"
ログのフォーマットを設定する処理は、このサーブフロー内のみで行われるため、ログフォーマットの統一が容易である。
フローの中のどこでログを出力させるかは、取得したログの利用目的に依存する。 ただし、多くの場合は、以下の箇所でログを出力するのが良いだろう。
- ノードのエラーが発生した際の、エラーハンドリングフロー内
- 正常系フローにメッセージが投入されたことを検知できる箇所
- 正常系フローで、処理が完了したことを検知できる箇所
- フロー内で状態の管理を行っている箇所
特に、状態の管理を行っている箇所は、フローベースプログラミングにおいて、仕様上の問題やバグが潜みやすい。 そのため、このような通常よりリスクが高いフローを記載する箇所でもINFOレベルやDEBUGレベルでログを出力させておくのが良いだろう。
状態には、コンテキストの状態だけでなく、DBとのアクセスなどのノードの状態も含まれる。 ノードの状態は、Statusノードを利用して、状態の変化をログに記録する。
反対に、ログを出力する際に注意が必要な箇所は、msg
が機密性の高いデータを扱っている時である。
例えば、パスワードを扱う場合、パスワードを含めてログに出力してよいか、検討する必要がある。
もし、パスワードを扱っていることをログに残す必要があれば、パスワード部分を伏字にする必要がある。
ソフトウェアでエラーが発生した際に、そのエラーに対する適切な処理を用意しておくことを、エラーハンドリングという。 例えば、指定したファイルにアクセスできない、入力データが想定した形式ではない、などの実行時エラーが発生すると、システムの振る舞いが予測不能となり、場合によってはシステム全体の誤動作やデータ破損に繋がる可能性がある。 そこで、エラーの発生を知らせるエラーイベントを捉え、エラーに対処するための処理を実行する、エラーハンドリングを用意する。 例えば、エラー内容のログ出力、代替処理への切り替え、人間の判断を待つ、意図的にシステムを安全に終了する、などの処理を用意する。 Node-REDでも、エラーハンドリングが必要である。 エラーハンドリングの処理は、他の正常系の処理と同じく、フローで表現する。 以下では、Node-REDでのエラーハンドリングの実装方法と、エラーハンドリングを行うフローの配置について説明する。
Node-REDの場合、ノードの処理中に発生したエラーには、catchノードを利用してエラーハンドリングを実装出来る。
catchノードをフローの最初のノードとし、その後エラーハンドリング処理をフローで記述する。
catchノードが捉えることができるエラーは、任意のノードで処理中に発生したエラーと、functionノード内にフロー開発者によって記述されたnode.error("エラー内容", msg)
で意図的に発生したエラーの2種類である。
「functionノード内エラー」には、以下のコードが記載されている。
process_success_done = false;
if (process_success_done){
node.status({fill:"green",shape:"dot",text:"success"});
}else{
node.status({fill:"red",shape:"ring",text:"fail"});
node.error('[E0001] ファンクション内エラー', msg); // 意図的に例外を発生させる
}
catchノードは、同じタブ内のノードからのエラーを取得する。
ただし、もしサブフロー内に、サブフロー内で発生したエラーに対応するcatchノードがない場合、サブフローを利用しているフローにエラーイベントが伝播する。
これは、Java等でメソッドのエラーを呼び出し元に伝播させる処理(throws
)に近い。
フロー開発者はこの特性を理解・応用し、エラーへの対処の見通しが良いフローを設計する必要がある。
まず、発生しうるエラーが、サブフロー内で処理すべきエラーか、呼び出し元で対処すべきエラーなのかを検討し、そして、サブフロー内にエラーに対応するcatchノードを設置するかを検討する。
例えば、サブフローの入力msg
が不正な場合は、サブフロー内にエラーに対応するcatchノードを設置せずに、呼び出しもとのフローにエラーを伝播させる。
エラーハンドリングを行うフローは正常な処理中とは異なる処理であるため、多くの場合、エラーハンドリングのフローに業務プロセスは含まれない。 業務プロセスを表す正常系のフローと、業務プロセスを含まないフローが入り乱れると、フローを閲覧するユーザーにとっては可視性が悪い。 そのため、正常系のフローとエラーハンドリングを行うフローは、明示的に異なる位置に記述するのが良い。 ただし、正常系のフローと、それに対応するエラーハンドリング用のフローが、遠くに記述されていると、フローの見通しが悪く、誤った対応を設定する可能性も高くなる。 そこで、本ガイドで厳格な指定はしないが、フローの並べ方として、以下二つを提案する。
- 一つの正常系のフローに対し、そのフローに関するエラーをすぐ下に記述する (下図左)
- 正常系のフローをタブ内の上部にまとめ、エラーハンドリングのフローは正常系のフローの順番にタブの下部に上から記載する
フロー開発者は、フローの開発後、可読性を向上させるために、リファクタリングを行うことを推奨する。 基本的には、本ガイドラインに記載の内容を網羅していることが望ましいが、この節では、リファクタリングとして特に抑えておくべきポイントをリスト化して示す。
開発したフローを、他者に提示する前や、テンプレート化知る前には、このチェックリストを確認し、開発したフローがこれらの項目を満たしているか確認して頂きたい。
- コーディングスタイル
- 業務の流れを反映し、システム全体の処理の流れがイメージできるフロー(業務レベルのフロー)が存在する
- メッセージ内に格納される、処理対象のデータは、
msg.payload
に格納されている - 不必要に
msg.
以下の属性(msg.payload
以外)の値を修正していない - 業務レベルのフロー内のノードは、業務処理が理解できる名前が付けられている
- 処理レベルのフローは、「動詞+目的語」または「動詞+目的語+副詞」で名前が付けられている
- functionノードが多用されていない
- フローが、処理内容ごとに分けて整列されている
- フローの実装
- ライブラリとして、業務フローの随所で再利用されるフローが、サブフローになっている
- システムの外部からの処理要求を受けないフロー(同一のNode-REDフローエンジンからのみ利用されるフロー)は、linkノードで他のフローと連携している
- グローバルコンテキストへの値の代入は、ある一つのサブフロー内でのみ行っている
- ループがある場合、ループから抜ける手段を用意している
- 信頼性・品質向上
- フローとサブフローには、コメントが付いており、処理の概要と入出力
msg
の説明が記述されている - 一つのフローには、commentノードが多くとも1~2個程度であり、処理の詳細はコメントに記載していない
- フローの稼動状況や障害を示すログを残している
- 適切なエラーハンドリングフローが存在する
- エラーハンドリングのフローは、そのエラーに対処すべきフローの中に実装されている
- フローが異常停止しても再開できるように、コンテキストのデータが永続化されている
- 障害からの復旧後に、同じエラーで繰り返し障害停止しないようにするための工夫が存在する
- フローとサブフローには、コメントが付いており、処理の概要と入出力
複数のタブが存在する中規模のフローや、多数の業務ドメインが連携する大規模なフローを開発する際は、開発者が複数人になり、開発期間も長期になるため、プロジェクト体制を構築する必要がある。 本節では、プロジェクト体制を構築する際の手順と、用意する開発環境について整理する。 また、ドメインの理解やストーリー・エピック管理、バックログの管理は、他のシステム開発と同様である。 必要に応じて、HIPACE IoT版を活用して頂きたい。 また、テスト方針については、本ガイドライン「信頼性・品質向上」を参照して頂きたい。
プロジェクト体制の構築は、主に以下のステップで実施するのが良い。
- プロジェクトビジョン策定・チームビルディング
- プロダクトバックログ構築
- テスト方針・教育方針作成
まず、プロジェクトを遂行する組織の現状の構造と能力を把握し、プロジェクトで使用する開発プロセスの決定及び文書化をし、承認を得る。 また、プロジェクトを進める前に、顧客の業務やターゲット市場の業界、ビジネスケースの理解に努める。 これらの理解は、ユーザーストーリーの準備やプロジェクトチームに必要な教育を選定する際に有用である。 そして、プロジェクトの目標、目的、制約事項を特定し、文書化し、チームメンバー間で共有する。
その後、適切な顧客部門の代表者もしくは社内の専門家と議論し、システムアーキテクチャを選択し文書化する。 システムアーキテクチャは、本ガイドラインの「アーキテクチャ設計」を参照し、プロジェクトチーム内で議論する。 ドメインおよびシステムアーキテクチャを策定したら、プロジェクト体制を構築する。 プロジェクト内に複数のチームが存在しうる場合は、各チームの責務と責務範囲が分かるように体制表を文書化して管理する。 この時には、プロジェクトで用いる用語や、アーキテクチャ、設計・開発指針等のプロジェクト標準も決定し、文書化する。
最後に、テスト方針と教育方針を作成する。 開発されるフローに期待する処理のふるまいを定義し、その振る舞いの正しさを検証できるテスト項目を定義する。 そして、テストの目的と観点、手順を文書化する。 ここまでで検討したドメイン知識・アーキテクチャ・開発手法・テスト方針を踏まえ、 チームメンバーのスキルセットを確認し、必要なスキルを有していないメンバーがいる場合は、教育を行う。 そのために、教育計画を作成・更新する。
前提として、フローを開発者Aと開発者Bで共同開発し、開発者CがGitの開発用ブランチで開発されたフローを、リリース用ブランチにマージする。
Node-REDには、一つのNode-REDのエンジンに対して、複数のフロープロジェクトを保存して切り替えて開発する機能がある。 この機能を、Project機能という。 プロジェクト機能はGitと連携可能であるため、Project機能を用いることで、フローをGitでバージョン管理できる。 そのため、メンバー間での共有にGitHubやGitBucketなどのGitリモートリポジトリを用いるのが良い。 また、GitHub RunnerのようなCIツールを用いることで、フローのテスト等を自動化できる。
これ以降では、GitリモートリポジトリにGitHub、CIツールにGitHub Runnerを用いるとして説明を続ける。
各開発者のローカル環境には、Node-REDとGitをインストールする。 Node-REDとGitは、全開発者のローカル環境には、同じバージョンのものをインストールする。 なお、Node-REDは、バージョン0.18以降を選択し、Projects機能(参照)を有効にする。 Gitは、バージョン2.0以降を選択する。その理由は、バージョン2.0以降でないと、Node-REDのProjects機能が正常に機能しないためである。
GitHubには、一つのプロジェクトに対して、開発用とリリース用の2つのブランチを用意する。 開発は開発用ブランチで管理するフローに対して行う。 特定のマイルストーンまで開発が完了したら、開発者Cはリリース用ブランチにフローをマージする。
プロジェクトの開始後、まず初めに行うことは、GitHubで空のプロジェクトを作成することである。 その後、任意の開発者が、Node-REDのProjects機能を用いて、ローカル環境にプロジェクトを設置する。 Projects機能でプロジェクトを新規作成する画面を開き、"Clone Repository"を選択し、"Git repository URL"に、GitHubで作成したプロジェクトのgit clone用URLを入力する。
ローカル環境にプロジェクトを設置した後、ローカル環境で開発用ブランチを作成し、GitHubにPushする。 ブランチ作成とGitHubへのPushは、Node-REDのProjects機能で実行できる。 ここまでの作業が完了したら、各開発者は、プロジェクトの開発作業を開始する。
開発者Aと開発者Bは、Projects機能を用いて、ローカル環境にGitHubからプロジェクトを設置する。
もし全ブランチがローカル環境に設置できない場合は、コンソール上で、git pull --all
を行い、全ブランチを取得する。
その後、開発用ブランチに切り替え、フローの開発を進める。
開発者Aと開発者Bは、フローを開発し終えたら、まずNode-REDの画面上でgit pull
を行う。
この際、コンソールでのgitコマンドで処理することは避け、Node-REDのProjects機能を利用する。
もし、他の開発者がフローを修正し、GitHubにPushしていた場合には、git pull
の際に必ず競合が発生する。
ただし、同じノードに対する修正が競合していない限りは、Node-REDが自動的に競合を解決することができる。
もし同じノードに対する修正が存在した場合は、開発者が手動で競合を解決しなければならない。
その場合、Node-RED上で手動で競合を解決するための画面を表示し、開発者は競合する二つの更新のうち、どちらを採用するか選択する。
開発者Cが、リリース用ブランチと開発用ブランチを、ターミナル上でgit pull
し、開発用ブランチの内容をリリース用ブランチに、git merge
する。
開発者Aと開発者Bが、リリース用ブランチに直接git push
を行っていなければ、競合が発生することはないため、開発者CはNode-REDのProjects機能を利用しても、コンソール上でgitコマンドを利用しても良い。
フローのバージョン管理および開発者間での共有は、Gitで行うことを推奨する。 その理由は、Node-REDでもProjects機能においてGitを利用できるためである。 フローの管理は、開発者がコンソール上でGitコマンドを実行することは極力避け、常にNode-REDのProjects機能を利用するべきだ。 その理由は、開発されるフローは、1行のみのJSONファイルとして保存されるため、コンソールでマージすると、競合する二つのフローのうち、どちらかを全て消去せざるを得なくなるためである。
下図は、Gitコマンドでのマージ操作により、Gitによってフローファイル内に自動記載される、競合箇所の表示内容を示している。 Gitの競合は、ファイルの各行に対して検査されるが、フローファイルは1行で表現されるため、競合箇所の比較が、フロー全体の比較となってしまう。 そのため、フローの中での、競合がある箇所のみを特定して解決することができない。
Node-REDのProjects機能を用いると、開発者はフローの修正に関する競合がNode-REDの画面上で検知することができ、競合の解決を行うことができる。 特に、Node-REDでは、競合が各ノードに対して検査されるため、フロー内での競合がある箇所のみを特定し、開発者が解決することができる。
なお、Node-REDのProjects機能でサポートされていないGitの機能は存在する。
例えば、Node-RED 0.18では、Revertに対応する機能は、Projects機能で提供されていない。
このように、Projects機能で提供されていないGitの操作に関しては、コンソールのGitコマンドを用いる必要がある。
Revertを行う際には、Node-REDで閲覧できるフローの修正履歴に、GitのコミットIDも表示されるため、git revert
の引数として参照できる。
また、"フローの処理単位"で説明したとおり、開発方針として、各タブには個別のドメインに関する処理を記述することで、フローの見通しが良くなることを説明したが、これに加え、開発者がそれぞれ別のタブを開発・修正するように取り決めることで、同一のノードに対する競合が発生する可能性は低くなり、Node-REDによる自動競合解決が期待できる。 できる限り、複数の開発者が一つのタブ内を修正することがなく、手動で競合の解決をしなくて済むように、プロジェクト内での開発方針を取り決め、プロジェクト標準にしておくべきである。
概要
- ノードの実行が長時間になるとNode-REDインスタンス全体の実行が停止してしまうため、他のサービスを稼働させ、処理を委託する。
メリット
- Node-REDインスタンスのスループット向上とハングアップの抑止
留意点
- コンテナ化によってプロセス間通信オーバヘッドが生じる可能性がある。
解説 Node.jsの実行モデルはシングルスレッドのため、あるノードの実行が長時間になるとNode-REDインスタンス全体の実行が停止し、ハングアップした状態となる可能性がある。 このような長時間実行ノードをコンテナ化し、独自ノードを介して非同期に呼び出すようにすることでこのような問題の発生を抑止することができる。
なお、長時間化の原因が何らかの条件成立を待つためであれば、Node.jsの非同期プログラミングの利用を推奨する。
概要
- Node-REDではメッセージの到着順序の保証がないため、メッセージの順序関係を表現する形式(分離メッセージ形式と呼ぶ)で関連するメッセージを表現する
メリット
- メッセージの間に順序関係がある場合、それを保証することができる。
- データを小さな論理的まとまりに分割して個別に処理し、後で統合することができる。
- 分散並列処理の適用が容易となる。
留意点
- 順序保証のためのメッセージ内情報が途中のノードで破棄されないことを確認する
解説 トランザクションデータ、ソートされたレコードなど複数のメッセージに順序関係をもたせたいことがある。Node-REDではノードへのメッセージの到着順は保証されないため、メッセージの到着順序によってメッセージ間の順序関係を暗黙的に表現することはできない。また、分散並列処理を導入した場合にも同様の課題が生じる。
Node-REDでは、データを複数のメッセージに分解するsplitノード、splitノードによって分割されたノードを順序を保って結合するjoinノードが標準ノードとして定義されている。本ガイドでは、これを分割メッセージ形式と呼ぶ。分割メッセージ形式の持つプロパティを下の表に示す。
分割メッセージ形式のプロパティ
項番 | プロパティ | 意味 | 備考 |
---|---|---|---|
0 | payload | 分割されたデータ | 必須 |
1 | parts.id | メッセージグループの識別子 | 必須 |
2 | parts.index | グループ内の順序 | 必須 |
3 | parts.count | グループに所属するメッセージの総数 | 必須 |
4 | parts.type | メッセージの型 (string/array/object) | |
5 | parts.ch | stringを分解した際の文字 | |
6 | parts.key | objectを分解する際に使ったキーの値 |
分割メッセージ形式を用いることで、あるメッセージグループに所属するメッセージを全て受け取ったか否か(parts.count個のメッセージを受け取ったか否か)の確認、メッセージグループに所属するメッセージの順序関係の保証(parts.indexの比較)を行うことが可能となる。 また、分割メッセージ形式のメッセージに対して、メッセージグループ内のメッセージのソート、フィルタリング、分散並列処理などを行うノードを今後用意する予定であり、分割メッセージ形式を採用することでこれらの適用が容易となる。
なお、現状の分割メッセージ形式ではメッセージグループ内のメッセージに関して次のような制限がある。
- メッセージの増減がある場合に、全てのノードのparts.countプロパティを変更する必要がある、
- グループに所属するメッセージ総数が不確定な場合の扱いが煩雑。
これらの問題を解決する分割メッセージ形式の拡張を今後検討する予定である。
Facade(窓口)パターンは、フローの中に、開発者が異なる部分が混在している時に、関心の分離が出来るようにフローを分ける。
まず、下図を見てほしい。 下図は、会議と座席の管理を行うフローであり、このフローを使うユーザーは、会議の設定と席の予約を行っている。 二つのfunctionノードは、会議の設定と座席の予約の複雑な処理を表しており、多くの場合は多数のノードの連なりである。
このフローは、2種類の人物に操作されることが想像できる。 一人は、会議の設定と座席の予約という、このフローを"使う"ユーザーであり、もう一人は会議・座席の管理システムの開発者である。
このフローは、フローの利用者が、自身の関心外の処理まで把握する必要がある、問題のあるフローだ。
前者のユーザーは、会議・座席の管理を行うために、functionノードの中身を見て、functionノードが自身期待する動作であるかや、functionノードに投入するmsg
の用意しなければならない。
そこで、Facadeパターンを使った下図のフローを見てほしい。 会議・座席の管理システムの開発者が記述するフローである。
このサブフローには、会議・座席の管理に関する複数(ここでは5種類)のサービスを用意している。
このサブフローを使うユーザーは、サブフローの入力msg.topic
に、利用するサービスを識別するための値(サービス名)を入れる。
サブフロー内のSwatchノードがmsg.topic
に格納されたサービス名の値を識別し、後段のサービスが実行される。
サブフローが提供するサービスとサービス名は、サブフローのコメントに記載しておく。
会議の設定と座席の予約をするユーザー用のフローだ。
上記のサブフローを、利用したいサービス名のみmsg.topic
に入れて利用する。
すなわち、フローを分け、一方のフローから利用される側のフローに窓口となるインターフェースを用意する。 これをFacadeパターンと呼ぶ。
こうすることで、ユーザーと開発者の関心が分離され、ユーザーはサービス名をmsg.topic
に入れるのみで、フローを利用できる。
また、開発者は、会議・座席の管理システムの実装や拡張を自由に行える。 さらに、開発者側にもメリットがあり、サービスを一つのサブフロー内にまとめ、サービス名で管理することで、どのサービスがどの順番で利用されたかなどのロギングや、エラーハンドリングなどが、サービス群で統一的に記述できる。
- 前提
- あるレイヤーにおいて、同等の処理を行う複数の処理が実装されている
- それらのフロー間では、相互に依存しあう (メッセージを交換し合う)
- 問題
- それらの全フロー間が、linkノードでつながる必要がある
- しかし、フロー間で密結合になる
- フローを拡張するたびに、いちいちリンクノードの設定を修正しないといけない
- 解決方法
- 複数のフロー間で、処理を調整する仲介フローを用意する
- 各フローでは、
msg
内のtopic
などに、メッセージを送りたい先を設定して仲介フローに送れば、仲介フローが、それに基づいて適切なフローに送る
- 効果
- 各フローが仲介フローにだけlinkノードで依存していたら良く、フロー間が疎結合になる
- フローが修正された場合や、新しいフローが追加された場合に修正する箇所が少なくなる
グローバルコンテキストは、基本的に利用しないようにするほうが、フロー全体の拡張性が向上する。 グローバルコンテキストへの値代入を、任意の箇所で行うと、思わぬ値がグローバルコンテキストに混入し、さらに値を代入した箇所の特定が困難、といった開発上の問題があるためだ。 しかしそれでも、フロー全体に対する初期設定値や環境変数として、利用した場合もある。
そこで、以下に示すPrivate global Contextパターンを利用する。
まず、グローバルコンテキストへの値代入をサブフローで用意し、このサブフロー内で、グローバルコンテキストの管理を一元化する。 このサブフローによる一元管理自体を、Private global Contextパターンと呼ぶことにする。 これを応用することで、例えば、グローバルコンテキストへの不用意な値代入を検出や、グローバルコンテキストへの値代入箇所の検索の容易化が、可能となる。
ポイントは、中間にあるchangeノード(change: 2 rules)であり、ここでは下図のように、他のフローから参照されるglobal.ENV_VAR
だけでなく、global.__ENV_VAR
にも同じ値を入れている。
その一つ前段のswitchノード(ENV_VAR==__ENV_VAR
)で、前にこのサブフローで変更したグローバルコンテキストの値を確認している。
万が一、フローのどこかで、global.ENV_VAR
が不用意に書き換えられていた場合、このswitchノードでそれを検出する。
最初のswitchノードで、メッセージ内にmsg.set_env_var
が存在し、値がtrue
かで条件分岐する。
これは、フロー全体の実行時の環境変数などの設定に用いられる。
この場合は、global.ENV_VAR
とglobal.__ENV_VAR
の値に関係なく、グローバルコンテキストの値変更が行われる。
フローの最後では、msg.set_env_var
は削除しておく。
グローバルコンテキストを利用するフローは、下図の通りであり、msg.set_global
を用意して上記のサブフローを呼び出すと、グローバルコンテキストの値を変えることが出来る。
この節では、不用意な値代入の検出を例にしたが、他にも、グローバルコンテキストへの値代入を行う箇所の検索を用意にすることも出来る。
msg.set_env_var
の設定のように、値代入の際に必ず何らかのパラメータを用意するようにすれば、Ctrl+Fで表示される検索フォームにそのパラメータ名を入力することで、フロー全体の中から値代入を行う箇所が特定できる。
フロー内で、msg
内の属性値を修正する際に、changeノードの値代入だけでは難しいケースがある。
例えば、「配列の各要素に特定の文字列を追加したい」「数値を文字列に変換したい」などである。
functionノードであれば、これらの処理を1ノードで実現することができるが、functionノードではフローを理解しにくくなる(参照:多用が推奨されない万能ノード)。
この場合、changeノードのJSONata記述機能が有用である。
JSONataとは、JSONオブジェクトを操作するためのクエリ言語であり、JSON内の構造の修正、値の抽出、変換など、多様な操作が実現可能である。
changeノードのJSONataを利用することによって、msg
の柔軟な値操作を実現しつつ、他のフロー閲覧者からmsg
の修正・代入を行っていることが理解されやすいフローが記述できる。
JSONataは、changeノード以外に、switchノードとsortノードでも利用可能である。 これらのノードでもJSONataを利用することによって、functionノードで代用することや、冗長なフローを記述する必要がなくなる。
changeノードでJSONataを用いた設定の例を示す。 この例では、華氏から摂氏への数値変換を、JSONataを用いて実現している。
さらに、JSONata式の記述を容易にするために、JSONata式内で使用可能な関数が、多数用意されている。 また、記述したJSONata式を試すテスト環境も用意されている。 これらの機能は、JSONata式を記述するフレームの右側にある三点リーダーボタンをクリックすることで、利用できる。
JSONataの構文などに関する詳細は、JSONataのドキュメントを参照して頂きたい。