イベント駆動型コードのメンテナンスを容易にする方法は?


16

イベントベースのコンポーネントを使用するとき、メンテナンス段階で苦痛を感じることがよくあります。

実行されたコードはすべて分割されているため、実行時に関与するすべてのコード部分を把握するのは非常に困難です。

これにより、誰かがいくつかの新しいイベントハンドラを追加したときに、微妙で困難なデバッグの問題が発生する可能性があります。

コメントから編集する:アプリケーション全体のイベントバスやハンドラーがアプリの他の部分にビジネスを委任するなど、オンボードのいくつかの優れたプラクティスを使用しても、多くのコードがあるためにコードが読みにくくなり始める瞬間があります多くの異なる場所から登録されたハンドラー(特にバスがある場合に当てはまります)。

その後、シーケンス図が複雑になり始め、何が起こっているのかを把握するのに時間がかかり、デバッグセッションが煩雑になります(ハンドラーの反復中のハンドラーマネージャーのブレークポイント、特に非同期ハンドラーとその上でのいくつかのフィルター処理で楽しい)。

///////////////

サーバー上のデータを取得するサービスがあります。クライアントには、コールバックを使用してこのサービスを呼び出す基本コンポーネントがあります。コンポーネントのユーザーに拡張ポイントを提供し、異なるコンポーネント間のカップリングを回避するために、いくつかのイベントを発生させています。1つはクエリが送信される前、1つは回答が返されるとき、もう1つは失敗の場合です。コンポーネントのデフォルトの動作を提供する事前登録された基本的なハンドラーセットがあります。

コンポーネントのユーザー(および私たちもコンポーネントのユーザー)は、いくつかのハンドラーを追加して、動作に何らかの変更を加えることができます(クエリ、ログ、データ分析、データフィルタリング、データマッサージ、UIファンシーアニメーション、チェーン複数の順次クエリの変更) 、 なんでも)。そのため、一部のハンドラーは他のハンドラーの前後に実行する必要があり、それらはアプリケーションのさまざまなエントリポイントから登録されます。

しばらくすると、十数人以上のハンドラーが登録されることがあり、それを操作するのは退屈で危険です。

この設計は、継承の使用が完全に混乱し始めたために生まれました。イベントシステムは、コンポジットの種類がまだわからないような構成で使用されます。

例の終わり
///////////////

だから私は他の人々がこの種のコードにどのように取り組んでいるのだろうと思っています。書き込み時と読み取り時の両方。

そのようなコードを苦労せずに記述および保守できる方法やツールはありますか?


あなたは、意味のほかにイベントハンドラからロジックをリファクタリング?
テラスティン

何が起こっているかを文書化します。

@Telastyn、「イベントハンドラーからロジックをリファクタリングする以外に」とはどういう意味かを完全に理解していない。
ギヨーム

@Thorbjoern:私のアップデートをご覧ください。
ギヨーム

3
仕事に適切なツールを使用していないようですね。つまり、順序が重要な場合は、アプリケーションのこれらの部分に最初から単純なイベントを使用しないでください。基本的に、典型的なバスシステムでイベントの順序に制限がある場合、それはもはや典型的なバスシステムではありませんが、それでもそのように使用しています。つまり、注文を処理するための追加のロジックを追加して、この問題に対処する必要があります。少なくとも、あなたの問題を正しく理解していれば..
stijn

回答:


7

内部イベントのスタック(より具体的には、任意の削除を伴うLIFOキュー)を使用してイベントを処理すると、イベント駆動型プログラミングが大幅に簡素化されることがわかりました。これにより、「外部イベント」の処理をいくつかの小さな「内部イベント」に分割し、その間に明確な状態を定義できます。詳細については、この質問に対する私の回答をご覧ください。

ここでは、このパターンによって解決される簡単な例を示します。

オブジェクトAを使用して何らかのサービスを実行し、それが完了したときに通知するコールバックを与えたとします。ただし、Aはコールバックを呼び出した後、さらに作業を行う必要がある場合があります。そのコールバック内で、Aがもう必要ないと判断し、何らかの方法でそれを破棄すると、危険が生じます。しかし、Aから呼び出されています-コールバックが戻った後、Aが破棄されたことを安全に把握できない場合、残りの作業を実行しようとするとクラッシュする可能性があります。

注:refcountのデクリメントのような他の方法で「破壊」を行うことができるのは事実ですが、それは中間状態になり、これらの処理からの余分なコードとバグにつながります。Aが必要なくなった後、何らかの中間状態で続行する以外は、Aが完全に動作を停止する方が良いでしょう。

私のパターンでは、Aは内部イベント(ジョブ)をイベントループのLIFOキューにプッシュすることで必要な作業を単純にスケジュールし、コールバックの呼び出しに進み、すぐにイベントループに戻ります。このコードは、Aが戻るだけなので、もはや危険ではありません。これで、コールバックがAを破棄しない場合、プッシュされたジョブはイベントループによって最終的に実行され、追加の作業が行われます(コールバックが行われ、プッシュされたすべてのジョブが再帰的に実行されます)。一方、コールバックがAを破棄する場合、Aのデストラクターまたはdeinit関数は、プッシュされたジョブをイベントスタックから削除し、プッシュされたジョブの実行を暗黙的に防ぎます。


7

適切なロギングは非常に大きな助けになると思います。スロー/処理されるすべてのイベントがどこかに記録されていることを確認してください(これにはロギングフレームワークを使用できます)。デバッグ中は、ログを参照して、バグが発生したときのコードの正確な実行順序を確認できます。多くの場合、これは問題の原因を絞り込むのに役立ちます。


はい、それは役に立つアドバイスです。生成されるログの量は、恐ろしいものになります(非常に複雑なフォームのイベント処理でサイクルの原因を見つけるのに苦労したことを覚えています)
ギヨーム

たぶん1つの解決策は、興味深い情報を別のログファイルに記録することです。また、各イベントにUUIDを割り当てることができるため、各イベントをより簡単に追跡できます。ログファイルから特定の情報を抽出できるレポートツールを作成することもできます。(または、コード内のスイッチを使用して、異なる情報を異なるログファイルに記録します)。
ジョルジオ

3

だから私は他の人々がこの種のコードにどのように取り組んでいるのだろうと思っています。書き込み時と読み取り時の両方。

イベント駆動型プログラミングのモデルにより、コーディングがある程度簡素化されます。これはおそらく、古い言語で使用されている大きなSelect(またはcase)ステートメントの置き換えとして進化し、VB 3などの初期のビジュアル開発環境で人気を得ました(歴史については引用しないでください。チェックしませんでした)。

イベントシーケンスが重要で、1つのビジネスアクションが多数のイベントに分割される場合、モデルは苦痛になります。このスタイルのプロセスは、このアプローチの利点に違反しています。どんな犠牲を払っても、対応するイベントにアクションコードをカプセル化し、イベント内からイベントを発生させないようにしてください。これは、GoToから生じるスパゲッティよりもはるかに悪くなります。

開発者は、そのようなイベントの依存関係を必要とするGUI機能を提供したい場合がありますが、実際には、非常に単純な実際の代替手段はありません。

ここで一番下の行は、賢明に使用されている場合、テクニックは悪くないということです。


「スパゲッティより悪い」を避けるための代替設計はありますか?
ギヨーム

期待されるGUIの動作を単純化することなく簡単な方法があるかどうかはわかりませんが、すべてのユーザーインタラクションをウィンドウ/ページでリストし、それぞれがプログラムの動作を正確に決定する場合、コードを1か所にグループ化して開始できます要求の実際の処理を担当するサブ/メソッドのグループへのいくつかのイベント。また、GUIのみに対応するコードをバックエンド処理を行うコードから分離することも役立ちます。
-NoChance

このメッセージを投稿する必要性は、継承地獄からイベントに基づいたものに移行したときのリファクタリングから生じました。いくつかの点では本当に優れていますが、他のいくつかではかなり悪いです...私はすでにいくつかのGUIで「イベントがワイルドになった」という問題を抱えていたので、そのようなメンテナンスを改善するために何ができるか疑問に思っていますイベントスライスコード。
ギヨーム

イベント駆動型プログラミングはVBよりはるかに古いです。それはSunTools GUIにありましたが、それ以前はSimula言語に組み込まれていたことを覚えているようです。
ケビンクライン

@ギョーム、あなたはサービスの設計をやり過ぎだと思います。私の上記の説明は、実際にはほとんどGUIイベントに基づいていました。このタイプの処理が(本当に)必要ですか?
NoChance

3

制御フローを「平坦化」および「平坦化」した後、ユーレカの瞬間が得られたため、この回答を更新したかったので、このテーマに関するいくつかの新しい考えを策定しました。

複雑な副作用と複雑な制御フロー

私が見つけたのは、私の脳は複雑な副作用、通常のイベント処理で見られるようなグラフのような複雑な制御フローに耐えることができますが、両方の組み合わせではありません。

シーケンシャルforループのような非常に単純な制御フローで適用されている場合、4つの異なる副作用を引き起こすコードについて簡単に推論できます。私の脳は、要素のサイズ変更と位置変更、アニメーション化、再描画、および何らかの補助ステータスの更新を行うシーケンシャルループに耐えることができます。それは理解するのに十分簡単です。

同様に、要素をマークするなど、順序が重要ではないプロセスで非常に単純な副作用が発生している場合は、カスケードイベントや複雑なグラフのようなデータ構造をたどることで、複雑な制御フローを理解できます単純な順次ループで遅延方式で処理されます。

私が迷い、混乱し、圧倒されるのは、複雑な制御フローがあり、複雑な副作用を引き起こしているときです。その場合、複雑な制御フローにより、最終的にどこに行き着くのかを事前に予測することが難しくなりますが、複雑な副作用により、結果として何が起こるかを正確に予測することは困難になります。そのため、これらの2つの要素の組み合わせにより、コードが今のところ完全に正常に動作していても、望ましくない副作用を引き起こすことを恐れずに変更するのがとても怖いのです。

複雑な制御フローは、いつ/どこで物事が起こるのかを推論するのを難しくする傾向があります。これらの複雑な制御フローが、ある事象がいつ/どこで起こるかを理解することが重要である副作用の複雑な組み合わせを引き起こしている場合にのみ、本当に頭痛の種になります。

制御フローまたは副作用を簡素化する

それで、あなたが理解するのがとても難しい上記のシナリオに遭遇するとき、あなたは何をしますか?戦略は、制御フローまたは副作用を単純化することです。

副作用を単純化するために広く適用可能な戦略は、遅延処理を優先することです。GUIのサイズ変更イベントを例として使用すると、通常の誘惑は、GUIレイアウトの再適用、子ウィジェットの再配置とサイズ変更、レイアウトアプリケーションの別のカスケードのトリガー、階層のサイズ変更と再配置、コントロールの再描画、場合によってはカスタムのサイズ変更動作を行うウィジェットの一意のイベント。who-knows-whereなどにつながるより多くのイベントをトリガーします。これをすべて1パスで行うか、イベントキューをスパムする代わりに、1つの可能な解決策はウィジェット階層を下るレイアウトの更新が必要なウィジェットをマークします。その後、簡単な順次制御フローを持つ後の遅延パスで、それを必要とするウィジェットのすべてのレイアウトを再適用します。次に、再描画する必要があるウィジェットをマークします。繰り返しますが、制御フローが単純な順次遅延パスで、再描画が必要とマークされたウィジェットを再描画します。

これには、制御フローが単純化されるため、制御フローと副作用の両方の効果があります。これは、グラフのトラバース中に再帰イベントがカスケードされないためです。代わりに、カスケードは遅延シーケンシャルループで発生し、その後、別の遅延シーケンシャルループで処理される可能性があります。より複雑なグラフのような制御フローでは、より複雑な副作用をトリガーする遅延シーケンシャルループで処理する必要があるものを単にマークするだけなので、副作用は単純になります。

これには処理のオーバーヘッドが伴いますが、たとえば、これらの遅延パスを並行して実行する可能性があり、パフォーマンスが懸念される場合は、開始したよりもさらに効率的なソリューションを取得できる可能性があります。ただし、ほとんどの場合、一般的にパフォーマンスはそれほど重要ではありません。最も重要なことは、これは議論の余地のある違いのように思えるかもしれませんが、私は推論するのがずっと簡単だとわかりました。何がいつ何が起こっているのかを予測するのがはるかに簡単になり、何が起こっているかをより簡単に理解できるようになることで得られる価値を過大評価することはできません。


2

私にとってうまくいったのは、他のイベントを参照せずに、各イベントを独立させることです。彼らはasynchroniouslyに来ている場合、あなたはしていない持っているシーケンスを、そうどのような順序で何が起こるかを把握しようとすることは不可能であることに加えて、無意味です。

最終的には、多数のデータ構造が読み込まれ、変更され、特定の順序ではなく多数のスレッドによって作成および削除されます。非常に適切なマルチスレッドプログラミングを行う必要がありますが、これは簡単ではありません。また、「このイベントでは、特定の瞬間に持っているデータを、それが何秒前だったか、何が変わったかに関係なく、見ていきます。 、そして、ロックを解除するのを待っている100個のスレッドがそれに対して何をしようとしているのかを考慮せずに、これに基づいて変更を行います。

私がしていることの1つは、特定のコレクションをスキャンし、参照とコレクション(スレッドセーフでない場合)の両方が正しくロックされ、他のデータと正しく同期されていることを確認することです。イベントが追加されると、この雑用が増えます。しかし、イベント間の関係を追跡していると、その雑用はずっと速く成長します。さらに、多くのロックが独自のメソッドで分離され、実際にコードが簡単になる場合があります。

各スレッドを完全に独立したエンティティとして扱うことは困難ですが(ハードコアマルチスレッド化のため)、実行可能です。「スケーラブル」は私が探している言葉かもしれません。2倍の数のイベントに必要な作業は2倍、たぶん1.5倍になります。より非同期のイベントを調整しようとすると、すぐに埋もれてしまいます。


質問を更新しました。シーケンスと非同期の処理については完全に正しいです。他のすべてのイベントが処理された後にイベントXを実行する必要がある場合は、どのように処理しますか?もっと複雑なHandlersマネージャーを作成する必要があるように見えますが、ユーザーにあまりにも多くの複雑さを見せないようにしたいです。
ギヨーム

データのセットに、イベントXの読み取り時に設定されるスイッチといくつかのフィールドがあります。他のすべてが処理されたら、スイッチをチェックし、Xを処理する必要があり、データがあることを確認します。実際には、スイッチとデータは独立している必要があります。設定したら、「Xを渡さなければならない」ではなく、「この仕事をしなければならない」と考える必要があります。次の問題:イベントが完了したことをどのように知るのですか?また、2つ以上のイベントXを受け取った場合はどうなりますか?最悪の場合、ループメンテナンススレッドを実行して、状況を確認し、独自のイニシアチブで行動できます。(3
秒間

2

ステートマシンとイベントドリブンアクティビティを探しているようです。

ただし、State Machine Markup Workflow Sampleも参照してください。

ここでは、ステートマシンの実装の簡単な概要を示します。ステートマシンワークフローは、ステートで構成します。各状態は、1つ以上のイベントハンドラで構成されます。各イベントハンドラーには、最初のアクティビティとして遅延またはIEventActivityが含まれている必要があります。各イベントハンドラには、1つの状態から別の状態への遷移に使用されるSetStateActivityアクティビティを含めることもできます。

各ステートマシンワークフローには、InitialStateNameとCompletedStateNameの2つのプロパティがあります。ステートマシンワークフローのインスタンスが作成されると、InitialStateNameプロパティに配置されます。ステートマシンがCompletedStateNameプロパティに到達すると、実行を終了します。


2
これは理論的には質問に答える可能性がありますが、答えの重要な部分をここに含め、参照用のリンクを提供することが望ましいでしょう
トーマスオーエンズ

しかし、各状態に多数のハンドラーが接続されている場合、何が起こっているのかを理解しようとすると、それほどうまくいきません...そして、イベントベースのコンポーネントはすべて、状態マシンとして記述されない場合があります。
ギヨーム

1

イベント駆動型コードは本当の問題ではありません。実際、コールバックが明示的に定義されているか、インラインコールバックが使用されている場合でも、ドリブンコードのロジックに従うのに問題はありません。たとえば、Tornadoのジェネレータースタイルのコールバックは、非常に簡単に追跡できます。

デバッグが非常に難しいのは、動的に生成される関数呼び出しです。地獄からコールバックファクトリーと呼ぶ(反?)パターン。ただし、この種の関数ファクトリは、従来のフローでデバッグするのも同様に困難です。

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