プレゼンテーションの分離
プレゼンテーションを操作するコードは、プレゼンテーションのみを操作し、すべてのドメインおよびデータソースロジックをプログラムの明確に分離された領域にプッシュするようにしてください。
2006年6月29日
これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる進展)」の一部です。残念ながら、それ以来、他の多くのことに注意を奪われており、それらをさらに進める時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料はまさに草案の段階であり、再び作業する時間を見つけるまで、修正や更新は行いません。
仕組み
このパターンはレイヤリングの一種であり、プレゼンテーションコードとドメインコードを別々のレイヤーに保持し、ドメインコードはプレゼンテーションコードを認識しません。このスタイルは、Model-View-Controllerアーキテクチャで流行し、広く使用されています。
これを使用するには、まずシステム内のすべてのデータと動作を調べ、そのコードがプレゼンテーションに関連しているかどうかを確認します。プレゼンテーションコードは、リッチクライアントアプリケーションのGUIウィジェットと構造、WebアプリケーションのHTTPヘッダーとHTML、またはコマンドラインアプリケーションのコマンドライン引数とprintステートメントを操作します。次に、アプリケーションを2つの論理モジュールに分割し、すべてのプレゼンテーションコードを1つのモジュールに、残りを別のモジュールに配置します。
データソースコードをドメイン(ビジネスロジック)から分離し、サービスレイヤーを使用してドメインを分離するために、さらにレイヤリングがよく使用されます。プレゼンテーションの分離の目的では、これらのさらなるレイヤーは無視し、これらをすべて「ドメインレイヤー」と呼ぶことができます。ドメインレイヤーのさらなるレイヤリングが行われる可能性があることに注意してください。
レイヤーは論理的な構成要素であり、物理的な構成要素ではありません。確かに、異なる層に物理的に分離されている場合がありますが、これは必須ではありません(そして、必要でない場合は悪い考えです)。また、異なる物理的なパッケージング単位(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); } }
それは良いことです、私は今ドメインの決定をドメインオブジェクトに持っています。しかし、物事は少し厄介です。プレゼンテーションは、実績の読み取りがヌルの場合、差異がヌルであることを知る必要はありません。そのような依存関係は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で読みやすいと思います。