分离式展示

确保任何操纵展示的代码只操纵展示,将所有领域和数据源逻辑推送到程序中明确分离的区域。

2006年6月29日

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

工作原理

这种模式是一种分层形式,我们将在单独的层中保留展示代码和领域代码,而领域代码不知道展示代码。这种风格随着模型-视图-控制器架构的流行而流行,并被广泛使用。

要使用它,您首先要查看系统中的所有数据和行为,并查看该代码是否与展示有关。展示代码将操纵富客户端应用程序中的 GUI 小部件和结构,Web 应用程序中的 HTTP 标头和 HTML,或命令行应用程序中的命令行参数和打印语句。然后,我们将应用程序划分为两个逻辑模块,所有展示代码在一个模块中,其余代码在另一个模块中。

通常使用进一步的分层来将数据源代码与领域(业务逻辑)分离,并使用服务层来分离领域。为了分离式展示的目的,我们可以忽略这些进一步的层,只将所有这些称为“领域层”。请记住,领域层的进一步分层是可能的。

这些层是逻辑的,而不是物理的。当然,您可能会发现物理上分离到不同的层,但这并不是必需的(如果不需要,这是一个坏主意)。您也可能会看到分离到不同的物理打包单元(例如 Java jar 或 .NET 程序集),但这同样不是必需的。当然,最好使用任何逻辑打包机制(Java 包、.NET 命名空间)来分离这些层。

除了分离之外,还有一个严格的可见性规则。展示可以调用领域,但反之则不行。这可以在构建过程中使用依赖项检查工具进行检查。这里的重点是领域应该完全不知道可能与它一起使用的展示。这既有助于将关注点分开,也支持使用多个展示与相同的领域代码一起使用。

虽然领域不能调用展示,但如果发生任何更改,领域通常需要通知展示。观察者是解决此问题的常用方法。领域触发一个事件,该事件由展示观察,然后展示根据需要从领域重新读取数据。

一个很好的心理测试,用于检查您是否正在使用分离式展示,是想象一个完全不同的用户界面。如果您正在编写 GUI,请想象为同一个应用程序编写一个命令行界面。问问自己,GUI 和命令行展示代码之间是否会有任何重复 - 如果有,那么它就是移动到领域的理想候选。

何时使用它

示例:将领域逻辑从窗口中移出(Java)

您从我这里看到的例子大多遵循分离式展示,仅仅因为我发现它是一种非常基本的设计技术。以下是一个示例,说明如何重构一个不使用分离式展示的简单设计,以便使用它。

该示例来自冰淇淋大气监测器运行示例。我用来说明这一点的主要任务是计算目标和实际之间的方差,并为字段着色以指示该方差的大小。您可以想象在评估窗口对象中完成此操作,如下所示

class AssessmentWindow...

  private JFormattedTextField dateField, actualField, targetField,
  varianceField;
  Reading currentReading;

   private void updateVarianceField() {
       if (null == currentReading.getActual()) {
           varianceField.setValue(null);
           varianceField.setForeground(Color.BLACK);
       }
       else {
           long variance = currentReading.getActual() - currentReading.getTarget();
           varianceField.setValue(variance);
           long varianceRatio = Math.round(100.0 * variance / currentReading.getTarget());
            if (varianceRatio < -10) varianceField.setForeground(Color.RED);
            else if (varianceRatio > 5) varianceField.setForeground(Color.GREEN);
            else varianceField.setForeground(Color.BLACK);
        }
   }

如您所见,此例程将计算方差的领域问题与更新方差文本字段的行为混合在一起。Reading 对象(包含实际数据和目标数据)在这里是一个数据类 - 一组贫血的字段和访问器。由于这是包含数据的对象,它应该是计算方差的对象。

要开始,我可以对方差计算本身使用“用查询替换临时变量”,以产生以下结果。

class AssessmentWindow...

  private void updateVarianceField() {
       if (null == currentReading.getActual()) {
           varianceField.setValue(null);
           varianceField.setForeground(Color.BLACK);
       }
       else {
           varianceField.setValue(getVariance());
           long varianceRatio = Math.round(100.0 * getVariance() / currentReading.getTarget());
            if (varianceRatio < -10) varianceField.setForeground(Color.RED);
            else if (varianceRatio > 5) varianceField.setForeground(Color.GREEN);
            else varianceField.setForeground(Color.BLACK);
        }
   }
   private long getVariance() {
       return currentReading.getActual() - currentReading.getTarget();
   }

现在计算在自己的方法中,我可以安全地将其移动到 Reading 对象中。

class AssessmentWindow...
   private void updateVarianceField() {
        if (null == currentReading.getActual()) {
            varianceField.setValue(null);
            varianceField.setForeground(Color.BLACK);
        }
        else {
            varianceField.setValue(currentReading.getVariance());
            long varianceRatio = Math.round(100.0 * currentReading.getVariance() / currentReading.getTarget());
             if (varianceRatio < -10) varianceField.setForeground(Color.RED);
             else if (varianceRatio > 5) varianceField.setForeground(Color.GREEN);
             else varianceField.setForeground(Color.BLACK);
         }
    }
class Reading...
    public long getVariance() {
        return getActual() - getTarget();
    }


我可以对 varianceRatio 做同样的事情,我将只显示最终结果,但我会分步进行(创建本地方法然后移动它),因为这样我出错的可能性更小 - 特别是使用我正在使用的重构编辑器(IntelliJ Idea)。

class AssessmentWindow...
   private void updateVarianceField() {
        if (null == currentReading.getActual()) {
            varianceField.setValue(null);
            varianceField.setForeground(Color.BLACK);
        }
        else {
            varianceField.setValue(currentReading.getVariance());
            if (currentReading.getVarianceRatio() < -10) varianceField.setForeground(Color.RED);
            else if (currentReading.getVarianceRatio() > 5) varianceField.setForeground(Color.GREEN);
            else varianceField.setForeground(Color.BLACK);
         }
    }

class Reading...
   public long getVarianceRatio() {
        return Math.round(100.0 * getVariance() / getTarget());
    }

计算看起来好多了,但我仍然不太满意。关于颜色应该是什么颜色或另一种颜色的逻辑是领域逻辑,即使颜色的选择(以及文本颜色是展示机制这一事实)是展示逻辑。我需要做的是将我们拥有的方差类别(以及为这些类别分配逻辑)的确定与着色分开。

这里没有正式的重构,但我需要在 Reading 上有一个这样的方法。

class Reading...
    public enum VarianceCategory {LOW, NORMAL, HIGH}

    public VarianceCategory getVarianceCategory() {
         if (getVarianceRatio() < -10) return VarianceCategory.LOW;
         else if (getVarianceRatio() > 5) return VarianceCategory.HIGH;
         else return VarianceCategory.NORMAL;
    }

class AssessmentWindow...
    private void updateVarianceField() {
        if (null == currentReading.getActual()) {
            varianceField.setValue(null);
            varianceField.setForeground(Color.BLACK);
        }
        else {
            varianceField.setValue(currentReading.getVariance());
            if (currentReading.getVarianceCategory() == Reading.VarianceCategory.LOW) varianceField.setForeground(Color.RED);
            else if (currentReading.getVarianceCategory() == Reading.VarianceCategory.HIGH) varianceField.setForeground(Color.GREEN);
            else varianceField.setForeground(Color.BLACK);
         }
    }

这样好多了,我现在在领域对象中有了领域决策。但事情有点乱,展示不应该知道如果实际读数为空,则方差为空。这种依赖关系应该封装在 Reading 类中。

class Reading...
   public Long getVariance() {
        if (null == getActual()) return null;
        return getActual() - getTarget();
    }

class AssessmentWindow...
    private void updateVarianceField() {
        varianceField.setValue(currentReading.getVariance());
        if (null == currentReading.getVariance()) {
            varianceField.setForeground(Color.BLACK);
        }
        else {
            if (currentReading.getVarianceCategory() == Reading.VarianceCategory.LOW) varianceField.setForeground(Color.RED);
            else if (currentReading.getVarianceCategory() == Reading.VarianceCategory.HIGH) varianceField.setForeground(Color.GREEN);
            else varianceField.setForeground(Color.BLACK);
         }
    }

我可以通过添加一个空方差类别来进一步封装它,这也允许我使用我发现更容易阅读的 switch。

class Reading...
  public enum VarianceCategory {
        LOW, NORMAL, HIGH, NULL}

    public VarianceCategory getVarianceCategory() {
        if (null == getVariance()) return VarianceCategory.NULL;
        if (getVarianceRatio() < -10) return VarianceCategory.LOW;
        else if (getVarianceRatio() > 5) return VarianceCategory.HIGH;
        else return VarianceCategory.NORMAL;
    }

class AssessmentWindow...
   private void updateVarianceField() {
        varianceField.setValue(currentReading.getVariance());
        switch (currentReading.getVarianceCategory()) {
            case LOW:
                varianceField.setForeground(Color.RED);
                break;
            case HIGH:
                varianceField.setForeground(Color.GREEN);
                break;
            case NULL:
                varianceField.setForeground(Color.BLACK);
                break;
            case NORMAL:
                varianceField.setForeground(Color.BLACK);
                break;
            default:
                throw new IllegalArgumentException("Unknown variance category");
        }
     }

作为最后一步,虽然与分离式展示无关,但我更喜欢清理该 switch 以消除重复。

class AssessmentWindow...
  private void updateVarianceField() {
        varianceField.setValue(currentReading.getVariance());
        varianceField.setForeground(varianceColor());
    }

    private Color varianceColor() {
        switch (currentReading.getVarianceCategory()) {
            case LOW:
                return Color.RED;
            case HIGH:
                return Color.GREEN;
            case NULL:
                return Color.BLACK;
            case NORMAL:
                return Color.BLACK;
            default:
                throw new IllegalArgumentException("Unknown variance category");
        }
    }

这使得很明显,我们这里有一个简单的表格查找。我可以通过填充和索引从哈希中替换它。在某些语言中,我可能会这样做,但我发现这里的 switch 在 Java 中很好且易读。