分離式簡報

確保任何處理簡報的程式碼只處理簡報,將所有網域和資料來源邏輯推入程式明確分開的區域。

2006 年 6 月 29 日

這是 進一步的企業應用程式架構開發 寫作的一部分,我在 2000 年代中期進行。遺憾的是,自那以後,太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,我也看不到在可預見的未來會有多少時間。因此,這份資料在很大程度上仍是草稿形式,而且我不會進行任何更正或更新,直到我找到時間再次研究它為止。

運作方式

此模式是一種分層形式,我們將簡報程式碼和網域程式碼保存在不同的層中,而網域程式碼不會察覺簡報程式碼。這種風格隨著模型檢視控制器架構而流行,並被廣泛使用。

要使用它,您必須先檢視系統中的所有資料和行為,並查看該程式碼是否與簡報有關。簡報程式碼會在豐富的用戶端應用程式中處理 GUI 小工具和結構,在網路應用程式中處理 HTTP 標頭和 HTML,或在命令列應用程式中處理命令列引數和列印陳述式。然後,我們將應用程式分成兩個邏輯模組,其中一個模組包含所有簡報程式碼,另一個模組包含其他所有程式碼。

進一步的分層通常用於將資料來源程式碼與網域(商業邏輯)分開,並使用 服務層 將網域分開。為了 分離式簡報 的目的,我們可以忽略這些進一步的層,只需將所有這些稱為「網域層」即可。請記住,網域層的進一步分層是可能的。

這些層是邏輯結構,而不是實體結構。當然,您可能會發現實體上分隔成不同的層級,但這不是必需的(而且如果沒有必要,這是一個壞主意)。您也可能會看到分隔成不同的實體封裝單元(例如 Java jar 或 .NET 組件),但這也不是必需的。當然,使用任何邏輯封裝機制(Java 套件、.NET 命名空間)來分隔這些層是好的。

除了分隔之外,還有一個嚴格的可見性規則。簡報可以呼叫網域,但網域不能呼叫簡報。這可以用依賴性檢查工具作為建置的一部分來檢查。這裡的重點是網域應該完全不知道可能會使用哪些簡報。這有助於將問題分開,並支援使用多個簡報與同一個網域程式碼。

儘管網域無法呼叫簡報,但網域通常需要在發生任何變更時通知簡報。觀察者 是解決此問題的常見方法。網域會觸發簡報觀察的事件,然後簡報會根據需要重新從網域讀取資料。

用於檢查您是否使用 分離簡報 的良好心智測驗是想像一個完全不同的使用者介面。如果您正在撰寫 GUI,請想像為同一個應用程式撰寫命令列介面。自問 GUI 和命令列簡報程式碼之間是否有任何重複的部分 - 如果有,則表示適合移至網域。

何時使用

範例:將網域邏輯移出視窗 (Java)

您會看到我提供的大部分範例都遵循 分離簡報,原因很簡單,因為我認為這是一種基本的設計技巧。以下是一個如何重構不使用 分離簡報 的簡單設計以使用它的範例。

範例來自冰淇淋大氣監測器的執行範例。我用來說明此範例的主要任務是計算目標值與實際值之間的差異,並為欄位著色以顯示此差異的量。您可以想像在評估視窗物件中以這種方式完成此操作

類別 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 物件在此是一個資料類別 - 一個貧血的欄位與存取項集合。由於這是擁有資料的物件,因此它應該是計算差異的物件。

為開始此操作,我可以在差異計算本身使用「以查詢取代暫存」來產生此結果。

類別 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);
         }
    }

這樣比較好,現在我在網域物件中有了網域決策。但事情有點混亂,如果實際讀數為 null,簡報不應該知道變異為 null。這種依賴性應該封裝在 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);
         }
    }

我可以透過新增 null 變異類別進一步封裝它,這也允許我使用我認為比較好讀的 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");
        }
    }

這使得很明顯,我們這裡有一個簡單的表格查詢。我可以透過從 hash 中填入和索引來取代它。在某些語言中,我可能會這麼做,但我發現這裡的 switch 在 Java 中很不錯且易於閱讀。