オブザーバー同期

複数の画面を、ドメインデータの共有領域に対するオブザーバーとすることで、同期します。

これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる発展)」の一部です。残念ながら、それ以来、他の多くのことが私の注意を引くようになり、それらをさらに進める時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料はまさにドラフト形式であり、再び作業する時間を見つけるまで、修正や更新は行いません。

一部のアプリケーションでは、共通のデータ領域のプレゼンテーションを表示する複数の画面が利用可能です。これらの画面のいずれかを通じてデータが変更された場合、他のすべての画面が正しく更新されるようにする必要があります。しかし、各画面が他の画面について認識していると、画面の複雑さが増し、新しい画面を追加することが難しくなるため、それは望ましくありません。

オブザーバー同期は、ドメイン指向の単一のデータ領域を使用し、各画面をそのデータのオブザーバーにします。1つの画面での変更は、そのドメイン指向データに伝播され、そこから他の画面に伝播されます。このアプローチは、Model View Controllerアプローチの大きな部分を占めていました。

仕組み

このアプローチの本質は、各画面が、関連する画面の状態とともに、セッションデータの共通領域のオブザーバーとして機能することです。セッションデータに対するすべての変更は、画面がリッスンしてセッションデータからリロードすることで応答するイベントになります。このメカニズムが設定されると、各画面がセッションデータを更新するようにコーディングするだけで、同期を確保できます。変更を行う画面でさえ、明示的に自身をリフレッシュする必要はありません。オブザーバーメカニズムは、別の画面が変更を行った場合と同じ方法でリフレッシュをトリガーするためです。

おそらく、この設計における最大の課題は、使用するイベントの粒度と、伝播とオブザーバーの関係を設定する方法を決定することです。非常に細かい粒度レベルでは、各ドメインデータビットは、何が変更されたかを正確に示す個別のイベントを持つことができます。各画面は、その画面のデータを無効にする可能性のあるイベントのみに登録します。最も粗い粒度の代替手段は、イベントアグリゲーターを使用してすべてのイベントを単一のチャネルに集中させることです。そうすれば、各画面は、ドメインデータの一部が変更された場合、それが画面に影響を与えるかどうかに関係なく、リロードを実行します。

いつものように、トレードオフは複雑さとパフォーマンスの間です。粗い粒度の approaches は、セットアップがはるかに簡単で、バグが発生する可能性が低くなります。ただし、粗い粒度の approaches は、画面の不要なリフレッシュを多く発生させ、パフォーマンスに影響を与える可能性があります。いつものように、私のアドバイスは、粗い粒度から始めて、実際のパフォーマンスの問題を測定した後にのみ、必要に応じて適切な細かい粒度のメカニズムを導入することです。

イベントは、コードを見ても呼び出しの連鎖を確認できないため、デバッグが難しい傾向があります。そのため、イベント伝播メカニズムをできるだけシンプルに保つことが重要であり、そのため、私は粗い粒度のメカニズムを支持しています。良い経験則は、オブジェクトを階層化されたものと考えて、階層間でのみオブザーバー関係を許可することです。そのため、1つのドメインオブジェクトは別のドメインオブジェクトを観察するべきではなく、プレゼンテーションオブジェクトのみがドメインオブジェクトを観察するべきです。

非常に注意すべきもう1つの点は、1つのイベントが別のイベントを発生させるイベントの連鎖です。このようなイベントチェーンは、コードを見ても動作を理解できないため、すぐに追跡するのが非常に難しくなる可能性があります。そのため、私はレイヤー内でのイベントを避け、イベントの単一行、またはイベントアグリゲーターを使用することをお勧めします。

オブザーバーがイベントに登録すると、サブジェクトからオブザーバーへの参照を取得します。画面を取り除いたときにオブザーバーがサブジェクトから自身を削除しないと、ゾンビ参照とメモリリークが発生します。各セッションでドメインデータを作成および破棄し、セッションが短い場合、これは問題にならない可能性があります。ただし、寿命の長いドメインオブジェクトは深刻なリークにつながる可能性があります。

いつ使うか

オブザーバー同期は、共通データを共有する複数のアクティブウィンドウがある場合に特に重要なパターンです。この状況では、ウィンドウが互いにリフレッシュするように信号を送るという代替手段は、各ウィンドウが他のウィンドウとそのリフレッシュが必要になる可能性のある時期を知る必要があるため、非常に複雑になります。新しいウィンドウを追加するには、情報を更新する必要があります。このアプローチでは、新しいウィンドウの追加は非常に簡単であり、各ウィンドウは共通のドメインデータとの独自の関係を維持するように設定できます。

オブザーバー同期の主な欠点は、イベントによってトリガーされる暗黙的な動作であり、コードから視覚化するのが困難です。その結果、イベント伝播のバグを見つけて修正するのが非常に難しくなる可能性があります。

オブザーバー同期は、より単純なナビゲーションスタイルでも使用できますが、これらのケースでは、フロ同期が合理的な代替手段です。オブザーバー同期の値は、更新にイベントを使用することで生じる複雑さを上回るほどではない場合があります。

参考文献

このパターンは明らかにオブザーバーと非常によく似ており、Model View Controllerの中心的な部分です。これとオブザーバーの主な違いは、これがオブザーバーの特定の用途であるということです。これは、Model View Controllerを構成する複数のパターンのこの部分が表示されます。

謝辞

Patrik Nordwallは、オブザーバーとメモリリークの問題を指摘しました。

例:アルバムとパフォーマー(C#)

図1:アルバムとパフォーマーを表示する画面。

図1のようなアプリケーションを考えてみましょう。パフォーマーの名前と彼らが出演するアルバムを編集するためのアクティブな画面があります。「Kind of Blue」アルバムのタイトルを編集する場合、編集内容がアルバム画面のテキストボックスとフォームタイトルだけでなく、Miles DavisとJohn Coltraneのリストエントリにも表示されるようにする必要があります。

図2:アルバムとパフォーマーのドメインオブジェクト。

この場合、私はパフォーマーとアルバムの簡単なドメインオブジェクトにドメインデータを保持しています(図2)。これらのクラスのいずれかでデータを変更する場合、イベントを伝播する必要があります。

クラスアルバム:DomainObject

  public string Title {
    get { return _title; }
    set {
      _title = value;
      SignalChanged();
    }
  }
  string _title;

クラスDomainObject ...

  public void SignalChanged() {
    if (Changed != null) Changed (this, null);      
  }
  public event DomainChangeHandler Changed;
public delegate void DomainChangeHandler (DomainObject source, EventArgs e);

ここでは、レイヤースーパタイプで定義された単純な変更イベントがあります。このイベントは、変更に関する情報を提供するのではなく、変更が発生したことを示すだけです。クライアントからの粗い粒度の同期に適しています。

フォームでは、アルバムを渡すことによって新しいアルバムフォームを作成します。コンストラクターは通常のGUI処理を行い、アルバム参照を設定し、イベントリスナーを接続し、最後にアルバムからデータを読み込みます。

クラスFrmAlbum ...

  public FrmAlbum(Album album)    {
    InitializeComponent();
    this._album = album;
    observeDomain();
    load();
  }
  private Album _album;
  private void observeDomain() {
    _album.Changed += new DomainChangeHandler(Subject_Changed);
    foreach (Performer p in _album.Performers) 
      p.Changed +=new DomainChangeHandler(Subject_Changed);
  }
  private void Subject_Changed(DomainObject source, EventArgs e)  {
    load();
  }

イベントリスニングは、依存ドメインオブジェクトの変更によってフォームがデータをリロードするように接続されています。

クラスFrmAlbum ...

  private void load() {
    txtTitle.Text = _album.Title;
    this.Text = _album.Title;
    lstPerformers.DataSource = performerNames();
  }
  private string[] performerNames() {
    ArrayList result = new ArrayList();
    foreach (Performer p in _album.Performers) 
      result.Add(p.Name);
    return (string[]) result.ToArray(typeof (string));
  }

アルバムのタイトルを変更する場合、基になるドメインオブジェクトで直接変更を行います。

クラスFrmAlbum ...

  private void txtTitle_TextChanged(object sender, EventArgs e) {
    this._album.Title = txtTitle.Text;
  }

各フォームが共有ドメインオブジェクトとのバインディングに参加できる限り、データバインディングを使用してこれの多くを実現できます。別のバリエーションは、イベントアグリゲーターを使用することです。これにより、各フォームはアグリゲーターのみに登録し、各ドメインオブジェクトに登録する必要がなくなります。パフォーマンスの問題がない場合は、そのようにします。ここでは、例をできるだけ互いに独立させておくことをお勧めします。

Supervising ControllerまたはPassive Viewを使用している場合、コントローラーはドメインイベントのオブザーバーとして機能します。プレゼンテーションモデルを使用している場合、プレゼンテーションモデルはオブザーバーとして機能します。