流程同步

根据用户在屏幕之间交互的流程,将屏幕与底层模型同步。

2004 年 11 月 15 日

这是我在 2000 年代中期进行的 企业应用架构进一步发展 写作的一部分。不幸的是,此后太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我能找到时间再次处理它。

应用程序通常包含多个显示相同数据的屏幕。如果在一个屏幕中编辑数据,则其他屏幕必须更新以反映这些更改。

流程同步 通过让屏幕代码在屏幕工作流程的适当点显式地重新同步来实现这一点。

工作原理

流程同步 是一种非常简单的解释方法。本质上,每次你执行更改跨多个屏幕共享状态的操作时,你都会告诉每个屏幕更新自身。问题是,这意味着,一般来说,每个屏幕在某种程度上都与应用程序中的其他屏幕耦合。如果你有很多屏幕以非常开放的方式工作,这可能会很糟糕 - 这就是 观察者同步 成为如此强大的替代方案的原因。

因此,流程同步 更适合简单的导航风格。

一种常见的风格是根和子风格。在这里,你有一个在整个应用程序中打开的根窗口。当你想要更详细的信息时,你打开一个子窗口,它通常是模态的。在这种情况下,使用 流程同步 很容易,你只需要在关闭子窗口时更新根窗口即可。如果你在根屏幕中执行此操作,子窗口不需要知道根窗口。

如果子窗口是非模态的,这可能会变得更加困难。如果你遵循只在子窗口关闭时更新数据(以及根窗口)的方法,这很简单。如果你想在子窗口仍然打开时进行更新,那么你正在走向 观察者同步 的领域。如果数据在子窗口之间共享,这也是有问题的。

另一种简单的风格是向导风格,你按顺序有一系列屏幕。使用向导,每个屏幕都是模态的,从一个屏幕移动到另一个屏幕涉及关闭旧屏幕并打开另一个屏幕。因此,在这种情况下,流程同步 很容易做到:在关闭屏幕时更新数据,并在打开下一个屏幕时加载新数据。你可以有一个带有根屏幕的模态子窗口的向导序列,其中关闭最后一个子窗口会更新根窗口。

何时使用它

流程同步观察者同步 的一种替代方法,用于将屏幕与域数据同步。在许多方面,它是一种更明确、更直接的方法。与难以看到和调试的隐式 观察者 关系不同,你拥有明确的同步调用,这些调用在代码中清晰地排列。

流程同步 的问题是,一旦你拥有不受限制的屏幕数量,这些屏幕可能会共享数据,事情就会变得非常混乱。任何屏幕上的任何更改都需要触发其他屏幕进行更新。通过对其他屏幕的显式调用来执行此操作,会使屏幕变得非常相互依赖。这就是 观察者同步 如此容易的原因。

尽管有其局限性,流程同步 确实有其用武之地。如果用户界面的导航流程很简单,并且一次只有一个或两个屏幕处于活动状态,它就能很好地工作。此类情况的示例包括屏幕序列(如向导)以及带有模态子窗口的根屏幕。 观察者同步 也适用于这些情况,但你可能会发现更明确的方法更可取。Web 用户界面实际上是一系列屏幕,因此可以有效地使用 流程同步,这是一个客户端/服务器连接协议使 观察者同步 非常难以实现的示例。

示例:根和子餐厅列表(Java)

在这个示例中,我有一些简单的屏幕。根是一个餐厅列表。要编辑列表,你点击餐厅条目以编辑详细信息。如果你更改了名称,则列表需要更新自身,包括名称内容和保持字母顺序。

餐厅类非常无聊,我不会在这里重复它。它只是与 UI 上的字段相对应的字符串字段以及相应的 getter 和 setter。这不是一个很好的类,但它不是本次讨论的重点,所以我允许它存在,尽管它很悲惨、可怜。

编辑餐厅详细信息的表单也很简单。我有一个全局更新监听器,我将其附加到每个字段,因此字段中的任何更改都会导致底层域对象和表单的全局刷新 - 这是粗粒度同步。

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());
   }

餐厅列表需要在两个点进行更新,在表单的初始创建时,以及在子窗口关闭时。我把更新列表的代码放在一个方法中。

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 响应缓慢,尤其是在列表很大时。