イベントを使用した分離コンポーネント間の通信


8

相互に作用する小さな(> 50)小さなWebComponentsたくさんあるWebアプリがあります。

すべてを切り離しておくために、原則として、どのコンポーネントも別のコンポーネントを直接参照できないようにしています。代わりに、コンポーネントはイベントを発生させ、(「メイン」アプリ内で)配線されて、別のコンポーネントのメソッドを呼び出します。

時間が経つにつれ、追加されるコンポーネントが増え、「メイン」のアプリファイルには次のようなコードチャンクが散らばっています。

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

これを改善するために、同様の機能をグループ化し、にClass関連性のある名前を付け、インスタンス化するときに参加要素を渡し、内の「配線」を次のClassように処理します。

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

そして、次のようにインスタンス化します。

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

私は自分自身のパターンを導入することに本当にうんざりしています。特にClasses、単なる初期化コンテナとして使用しているため、より良い言葉がないため、実際にはOOP-yではないパターンを導入しています。

私が見逃しているこのタイプのタスクを処理するためのよりよく知られた定義済みのパターンはありますか?


2
これは実際には少し素晴らしく見えます。
ロバートハーベイ

私が正しく覚えているとき 、あなたのあなたのこの前の質問であなたはすでにそれをメディエーターと呼びました、そしてそれはGoF本からのパターン名でもあります、それでこれは間違いなくOOPパターンです。
Doc Brown、

@RobertHarveyさてあなたの言葉は私に多くの重みを持っています。何か違うことをしませんか?私はこれを考えすぎているのかわかりません。
Nik Kyriakides

2
これを考えすぎないでください。あなたの「配線」クラスは私にはしっかりしているように見えます。それが機能し、名前に満足しているなら、それがどのように呼び出されても問題ありません。
Doc Brown、

1
@NicholasKyriakides:インターネットから私のような見知らぬ人ではなく、あなたの同僚(きっと私よりもシステムをよく知っている人)に良い名前を尋ねてください。
Doc Brown

回答:


6

あなたが持っているコードはかなり良いです。少し不快に思えるのは、初期化コードがオブジェクト自体の一部ではないということです。つまり、オブジェクトをインスタンス化できますが、そのワイヤリングクラスの呼び出しを忘れると、そのオブジェクトは役に立たなくなります。

通知センター(別名イベントバス)が次のように定義されているとします。

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

これはDIYのマルチディスパッチイベントハンドラです。その後、コンストラクタの引数としてNotificationCenterを要求するだけで、独自のワイヤリングを行うことができます。そこにメッセージを送信し、ペイロードを渡すのを待つのは、システムとの唯一の連絡先なので、非常にしっかりしています。

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

注:質問で使用したスタイルと一致させるため、および簡単にするために、キーにはインプレース文字列リテラルを使用しました。タイプミスのリスクがあるため、これはお勧めできません。代わりに、列挙型または文字列定数の使用を検討してください。

上記のコードでは、ツールバーは、関心のあるイベントのタイプをNotificationCenterに通知し、notifyメソッドを介してそのすべての外部対話を公開する責任があります。に関心のある他のクラスtoolbar-button-click-eventは、コンストラクタに登録するだけです。

このパターンの興味深いバリエーションは次のとおりです。

  • 複数のNCを使用してシステムのさまざまな部分を処理する
  • Notifyメソッドが逐次的にブロックするのではなく、通知ごとにスレッドを生成するようにする
  • NC内の通常のリストではなく優先リストを使用して、コンポーネントが最初に通知される部分的な順序付けを保証する
  • 後で登録解除するために使用できるIDを返す登録
  • メッセージ引数をスキップし、メッセージのクラス/タイプに基づいてディスパッチする

興味深い機能は次のとおりです。

  • NCの計測は、ロガーを登録してペイロードを印刷するのと同じくらい簡単です
  • 相互作用する1つ以上のコンポーネントのテストは、単にそれらをインスタンス化し、期待される結果のリスナーを追加し、メッセージを送信するだけです。
  • 古いメッセージをリッスンする新しいコンポーネントを追加するのは簡単です
  • 古いコンポーネントにメッセージを送信する新しいコンポーネントを追加するのは簡単です

興味深い落とし穴と可能な対策は次のとおりです。

  • 他のイベントをトリガーするイベントは混乱する可能性があります。
    • 予期しないイベントのソースを特定するために、イベントに送信者IDを含めます。
  • 各コンポーネントは、システムの特定の部分がイベントを受信する前に稼働しているかどうかを認識していないため、初期のメッセージがドロップされる可能性があります。
    • これは、「システム準備完了」メッセージを送信するコンポーネントを作成するコードで処理できます。もちろん、関心のあるコンポーネントは登録する必要があります。
  • イベントバスは、コンポーネント間に暗黙のインターフェイスを作成します。つまり、コンパイラが必要なすべてを実装したことを確認する方法はありません。
    • 静的と動的の間の標準的な引数がここに適用されます。
  • このアプローチでは、必ずしも動作ではなく、コンポーネントをグループ化します。システムを介してイベントをトレースするには、OPのアプローチよりも多くの作業が必要になる場合があります。たとえば、OPでは、保存に関連するすべてのリスナーを一緒にセットアップし、削除に関連するリスナーを他の場所に一緒にセットアップすることができます。
    • これは、適切なイベントの名前付けとフローチャートなどのドキュメントで軽減できます。(はい、ドキュメンテーションはコードと段階的に異なっています)。すべてのメッセージを取得するキャッチオールハンドラリストとポストキャッチオールハンドラリストを追加して、誰が何をどの順序で送信したかを出力することもできます。

このアプローチは、イベントバスアーキテクチャを連想させます。唯一の違いは、登録がメッセージタイプではなくトピック文字列であることです。文字列を使用することの主な弱点は、文字列を誤って入力する可能性があることです。つまり、通知またはリスナーのスペルが間違っている可能性があり、デバッグが困難です。
Berin Loritsch

私が知る限り、これはメディエーターの典型的な例です。このアプローチの問題は、コンポーネントをイベントバス/メディエーターと結合することです。たとえばbuttonsToolbar、イベントバスを使用しない別のプロジェクトにコンポーネントを移動する場合はどうなりますか?
Nik Kyriakides、2018年

+1メディエーターの利点は、文字列/列挙型に対して登録でき、クラス内で疎結合ができることです。オブジェクトの外部の配線をメイン/セットアップクラスに移動すると、すべてのオブジェクトが認識され、結合を気にすることなく、イベント/関数に直接配線できます。@NicholasKyriakides両方を使用するのではなく、どちらかを選択してください
Ewan

クラシックイベントバスアーキテクチャでは、メッセージ自体への結合のみが行われます。メッセージは通常、不変オブジェクトです。メッセージを送信するオブジェクトは、メッセージを送信するためにパブリッシャーインターフェイスのみを必要とします。メッセージオブジェクトのタイプを使用する場合は、メッセージオブジェクトを公開するだけで済みます。文字列を使用する場合は、トピック文字列とメッセージペイロードの両方を指定する必要があります(文字列がペイロードでない場合)。文字列を使用することは、両側の値に細心の注意を払わなければならないことを意味します。
Berin Loritsch

@NicholasKyriakides元のコードを新しいソリューションに移動するとどうなりますか?セットアップクラスを用意し、新しいコンテキストに合わせて変更する必要があります。同じことがこのパターンにも当てはまります。
Joel Harmon

3

以前は、ある種の「イベントバス」を導入していましたが、UIコードのイベントを伝達するために、ドキュメントオブジェクトモデル自体にますます依存するようになりました。

ブラウザでは、DOMは、ページのロード中であっても常に存在する1つの依存関係です。重要なのは、JavaScriptでカスタムイベントを利用し、イベントバブリングに依存してこれらのイベントを伝達することです。

サブスクライバーをアタッチする前に、「ドキュメントの準備ができるまで待機する」と叫ぶ前に、スクリプトがインポートされた場所やマークアップでの表示順序に関係なく、JavaScriptが実行を開始した瞬間からdocument.documentElementプロパティが<html>要素を参照します。

ここからイベントのリスニングを開始できます。

JavaScriptコンポーネント(またはウィジェット)をページの特定のHTMLタグ内に配置することは非常に一般的です。コンポーネントの「ルート」要素は、バブリングイベントをトリガーできる場所です。<html>要素のサブスクライバーは、他のユーザーが生成したイベントと同様に、これらの通知を受け取ります。

ボイラープレートコードの例:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

したがって、パターンは次のようになります。

  1. カスタムイベントを使用する
  2. document.documentElementプロパティでこれらのイベントをサブスクライブします(ドキュメントの準備が整うまで待つ必要はありません)。
  3. コンポーネントのルート要素またはでイベントを発行しますdocument.documentElement

これは、関数型とオブジェクト指向の両方のコードベースで機能します。


1

Unity 3Dでのビデオゲーム開発でも、同じスタイルを使用しています。Health、Input、Stats、Soundなどのコンポーネントを作成し、それらをゲームオブジェクトに追加して、そのゲームオブジェクトを構築します。Unityには、ゲームオブジェクトにコンポーネントを追加するメカニズムがすでにあります。しかし、私が見つけたのは、ほとんどの人がコンポーネントを照会したり、他のコンポーネント内のコンポーネントを直接参照していることでした(インターフェースを使用していたとしても、私が好むおかげでさらに結合されています)。他のコンポーネントとの依存関係がまったくなく、コンポーネントを分離して作成できるようにしたかったのです。したがって、データが変更されたときにコンポーネントにイベントを発生させ(コンポーネントに固有)、基本的にデータを変更するメソッドを宣言しました。次に、ゲームオブジェクトでクラスを作成し、すべてのコンポーネントイベントを他のコンポーネントメソッドに接着しました。

私がこれについて気に入っているのは、ゲームオブジェクトのコンポーネントのすべての相互作用を確認するために、この1つのクラスだけを見ることができるということです。Contactクラスは私のゲームオブジェクトクラスとよく似ているようです(MainPlayer、Orcなどのオブジェクトにゲームオブジェクトの名前を付けます)。

これらのクラスは、一種のマネージャークラスです。コンポーネント自体には、コンポーネントのインスタンスとそれらを接続するコード以外は何もありません。ここで、直接接続できる他のコンポーネントメソッドを呼び出すだけのメソッドを作成する理由がわかりません。このクラスのポイントは、実際にはイベントのフックアップを整理することです。

イベント登録の補足として、filterコールバックとargsコールバックを追加しました。イベントがトリガーされると(私は独自のカスタムイベントクラスを作成しました)、存在する場合はフィルターコールバックを呼び出し、trueを返す場合はargsコールバックに移動します。フィルターコールバックのポイントは、柔軟性を与えることでした。イベントはさまざまな理由でトリガーされる可能性がありますが、チェックがtrueの場合にのみフックされたイベントを呼び出します。例として、OnKeyHitイベントを持つ入力コンポーネントがあります。MoveForward()、MoveBackward()などのメソッドを持つMovementコンポーネントがある場合、OnKeyHit + = MoveForwardをフックできますが、キーを押して前方に移動したくありません。キーが「w」キーの場合にのみ、それを実行します。OnKeyHitが渡す引数を入力していて、そのうちの1つがヒットしたキーであるため、

私にとって、特定のゲームオブジェクトマネージャークラスのサブスクリプションは次のようになります。

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

コンポーネントは分離して開発できるため、複数のプログラマーがそれらをコーディングすることができます。上記の例では、入力コーダーは引数オブジェクトにKeyという名前の変数を与えました。ただし、Movementコンポーネントの開発者はKeyを使用していない可能性があります(引数を調べる必要がある場合、この場合はおそらくそうではありませんが、渡された引数の値を使用します)。この通信要件を削除するために、argsコールバックはコンポーネント間の引数のマッピングとして機能します。したがって、このゲームオブジェクトマネージャークラスを作成する人は、2つのクライアントを接続してその時点でマッピングを実行するときに、2つのクライアント間のarg変数名を知る必要があるだけです。このメソッドは、フィルター関数の後に呼び出されます。

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

したがって、上記の状況では、入力担当者はargsオブジェクト内の変数に「Key」という名前を付けましたが、Movementでは「keyPressed」という名前にしました。これは、コンポーネントが開発されているときにコンポーネント自体をさらに分離し、それをマネージャークラスの実装者に配置して、正しく接続するのに役立ちます。


0

価値があることについて、私はバックエンドプロジェクトの一部として何かをしており、同様のアプローチをとっています。

  • 私のシステムにはウィジェット(Webコンポーネント)は含まれていませんが、抽象的な「アダプター」、つまりさまざまなプロトコルを処理する具体的な実装が含まれています。
  • プロトコルは、可能な「会話」のセットとしてモデル化されます。プロトコルアダプターは、着信イベントに応じてこれらの会話をトリガーします。
  • 基本的にRxサブジェクトであるイベントバスがあります。
  • イベントバスはすべてのアダプターの出力をサブスクライブし、すべてのアダプターはイベントバスの出力をサブスクライブします。
  • 「アダプター」は、そのすべての「会話」の集約ストリームとしてモデル化されます。会話は、イベントバスの出力にサブスクライブされるストリームであり、ステートマシンによって駆動されるメッセージをイベントバスに生成します。

あなたの建設/配線の課題をどのように処理したか:

  • プロトコル(アダプターによって実装される)は、サブスクライブする入力ストリームに対するフィルターとして会話開始基準を定義します。C#では、これらはストリームに対するLINQクエリです。ReactJSでは、これらは.Whereまたは.Filter演算子になります。
  • 会話は、独自のフィルターを使用して関連メッセージを決定します。
  • 一般に、バスにサブスクライブされるものはすべてストリームであり、バスはそれらのストリームにサブスクライブされます。

ツールバーとの類似性:

  • ツールバークラスは、バスがサブスクライブされているツールバーイベントの監視可能な入力監視可能な(バス)の.Mapです。
  • ツールバーのオブザーバブル(複数のサブツールバーがある場合)は、複数のオブザーバブルがある可能性があるため、ツールバーはオブザーバブルのオブザーバブルです。これらはRxJであり、バスへの単一の出力にマージされます。

直面する可能性のある問題:

  • イベントが循環的ではなく、プロセスがハングすることを確認します。
  • 同時実行性(これがWebComponentsに関連するかどうかわからない):非同期操作または長時間実行される可能性のある操作の場合、バックグラウンドタスクとして実行されない場合、イベントハンドラーが監視可能なスレッドをブロックすることがあります。RxJSスケジューラーはこれに対処できます(たとえば、デフォルトでは、すべてのバスサブスクリプションのデフォルトスケジューラーを.ObserveOnできます)。
  • 会話の概念がないとモデル化できない、より複雑なシナリオ(例:メッセージを送信してイベントを処理し、それ自体がイベントである応答を待つ)。この場合、ステートマシンは、処理するイベント(モデル内の会話)を動的に指定するのに役立ちます。これを行うには、.filter状態に応じた会話ストリームを使用します(実際、実装はより機能的です。会話は、状態変化イベントのオブザーバブルからのオブザーバブルのフラットマップです)。

したがって、要約すると、問題のドメイン全体をオブザーバブルとして、または「機能的に」/「宣言的に」見て、バス(オブザーバブル)からバス(オブザーバー)も購読しています。オブザーバブル(たとえば、新しいツールバー)のインスタンス化は宣言型であり、プロセス全体を.map入力ストリームからのオブザーバブルのオブザーバブルとして見ることができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.