分離式簡報
確保任何處理簡報的程式碼只處理簡報,將所有網域和資料來源邏輯推入程式明確分開的區域。
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 中很不錯且易於閱讀。