フロー同期

画面間のユーザーインタラクションのフローに基づいて、画面と基盤となるモデルを同期します。

2004年11月15日

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

アプリケーションには、同じデータを表示する複数の画面が含まれていることがよくあります。ある画面でデータを編集した場合、変更を反映するために他の画面を更新する必要があります。

フロー同期は、画面コードがワークフローの適切な時点で明示的に再同期することでこれを実現します。

仕組み

フロー同期は、説明するのが非常に簡単なアプローチです。基本的に、複数の画面で共有されている状態を変更する何かを行うたびに、各画面に自身を更新するように指示します。問題は、一般的に、すべての画面がアプリケーション内の他の画面にある程度結合されていることを意味することです。非常にオープンエンドな方法で動作する画面がたくさんある場合、これは厄介になる可能性があります。そのため、オブザーバー同期は強力な代替手段となります。

したがって、フロー同期は、単純なナビゲーションスタイルの方が効果的です。

一般的なスタイルの1つは、ルートと子のスタイルです。ここでは、アプリケーション全体で開いているルートウィンドウがあります。より詳細な情報が必要な場合は、子ウィンドウを開きます。これは多くの場合モーダルです。この状況では、フロー同期を使用するのは簡単です。子ウィンドウを閉じるたびにルートを更新するだけです。ルート画面でこれを行う場合、子はルートを認識する必要はありません。

子がモーダルでない場合、これはより困難になる可能性があります。子ウィンドウが閉じられたときにのみデータを更新する(したがってルートも更新する)というアプローチに従えば、簡単です。子ウィンドウが開いている間に更新が必要な場合は、オブザーバー同期の領域に近づいています。子間でデータが共有されている場合も問題となります。

もう1つの単純なスタイルは、画面が順番に表示されるウィザードスタイルです。ウィザードでは、各画面はモーダルであり、ある画面から別の画面への移動には、古い画面を閉じて別の画面を開くことが含まれます。そのため、この状況では、フロー同期は簡単です。画面を閉じるときにデータを更新し、次の画面を開くときに新しいデータを読み込みます。ルート画面を持つモーダルの子ウィザードシーケンスを使用できます。最後の子を閉じるとルートが更新されます。

いつ使用するか

フロー同期は、ドメインデータと画面を同期するためのオブザーバー同期の代替手段です。多くの点で、より明示的で簡単な方法です。見たりデバッグしたりするのが難しい暗黙的なオブザーバー関係の代わりに、コードに明確にレイアウトされた明示的な同期呼び出しがあります。

フロー同期の問題点は、データを共有する可能性のある画面の数が無制限になると、事態が非常に混乱する可能性があることです。画面上の変更は、他の画面の更新をトリガーする必要があります。他の画面への明示的な呼び出しによってこれを行うと、画面の相互依存性が高くなります。これは、オブザーバー同期の方がはるかに簡単なところです。

制限はあるものの、フロー同期には確かに場所があります。ユーザーインターフェースのナビゲーションフローが単純で、一度に1つまたは2つの画面のみがアクティブである場合にうまく機能します。このような状況の例としては、画面のシーケンス(ウィザードなど)と、モーダルの子を持つルート画面があります。オブザーバー同期もこれらのケースで機能しますが、より明示的なアプローチの方が望ましい場合があります。Webユーザーインターフェースは事実上画面のシーケンスであり、したがってフロー同期で効果的に機能します。これは、クライアント/サーバー接続のプロトコルにより、オブザーバー同期の実行が非常に困難になる例です。

例:ルートと子のレストランリスト(Java)

例として、いくつかの簡単な画面を用意しました。ルートはレストランのリストです。リストを編集するには、レストランのエントリをクリックして詳細を編集します。名前を変更すると、リストは名前の内容とアルファベット順の両方を維持するために自身を更新する必要があります。

レストランクラスは非常に退屈なので、ここでは繰り返しません。UIのフィールドに対応する文字列フィールドと、それに付随するゲッターとセッターだけです。それほど優れたクラスではありませんが、今回の議論の焦点ではないので、その悲しい、哀れな存在を許容できます。

レストランの詳細を編集するフォームも非常にシンプルです。各フィールドにアタッチする単一のグローバル更新リスナーがあるので、フィールドの変更は基盤となるドメインオブジェクトとフォームのグローバルリフレッシュを引き起こします。これは粗粒度の同期です。

class RestaurentForm…

  public class FieldListener extends GlobalListener {
     void update() {
         save();
         load();
     }
  }
  private void load() {
      nameField.setText(model.getName());
      placeField.setText(model.getPlace());
      favoritesField.setText(model.getFavorites());
      directionsField.setText(model.getDirections());
  }
   private void save() {
       model.setName(nameField.getText());
       model.setPlace(placeField.getText());
       model.setFavorites(favoritesField.getText());
       model.setDirections(directionsField.getText());
   }

レストランリストは、フォームの最初の作成時と子ウィンドウが閉じられるたびに、2つの時点で更新する必要があります。リストを更新するコードをメソッドに配置します。

class RestaurentListForm...

  void load() {
      sortModel();
      list.setListData(getRestaurentNames());
  }

  private String[] getRestaurentNames() {
      String[] result = new String[model.size()];
      int i = 0;
      for (Restaurent r : model) result[i++] = r.getName();
      return result;
  }

  private void sortModel() {
      Collections.sort(model, new Comparator<Restaurent>() {
          public int compare(Restaurent r1, Restaurent r2) {
              return r1.getName().compareTo(r2.getName());
          }
      });
  }

次に、フォームのコンストラクターと、編集ボタンにアタッチされたアクションリスナーから呼び出します。

class RestaurentListForm...

  public RestaurentListForm(List<Restaurent> model) {
      this.model = model;
      buildForm();
      load();
      window.pack();
      window.setVisible(true);
  }
private class EditActionListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
        new RestaurentForm(window, selectedRestaurent());
        load();
    }
}

カスタムリストモデルとの比較

このアプローチの明らかな代替手段は、カスタムリストモデルを使用することです。上記の実装では、レストランのリストからデータのコピーを取得する組み込みリストモデルを使用しています。コピーの代わりに、基盤となるレストランのリストのラッパーにすぎない独自のリストモデル実装を提供することもできます。

private class RestaurentListModel extends AbstractListModel {
    private List<Restaurent> data;
    public RestaurentListModel(List<Restaurent> data) {
        this.data = data;
    }
    public int getSize() {
        return data.size();
    }
    public Object getElementAt(int index) {
        return data.get(index).getName();
    }
}

これで、レストランのリストにこのラッパーを作成し、作成時にリストウィジェットに提供します。こうすることで、リストは文字列の別のリストではなく、基盤となるレストランのリストをモデルとして使用します。レストラン名に加えた変更は自動的に画面に反映されるため、loadメソッドを記述または呼び出す必要はありません。

より難しい問題は、リストの並べ替え順序をどうするかということです。並べ替え順序がドメインクラスにとって意味がある場合は、名前を変更したときにレストランリストを再並べ替えする限り、そこで簡単に並べ替えることができます。ただし、多くの場合、並べ替え順序は画面に固有であり、異なる画面では異なる並べ替え順序が使用される場合があります。この場合、並べ替えスキームのトリガーはより問題になる可能性があります。代替手段は、要求されるたびにリストを並べ替えることですが、特にリストが大きい場合、UIが応答しなくなる可能性があります。