通知

ドメイン層のエラーやその他の情報を収集し、プレゼンテーション層に伝達するオブジェクト。

これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる進展)」の一部です。残念ながら、それ以来、他の多くのことが私の注意を引いており、それらをさらに取り組む時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料はまさに草稿段階であり、再び作業に取り組む時間を見つけるまで、修正や更新は行いません。

一般的なアプリケーションシナリオは、プレゼンテーション層がユーザーからデータを取得し、そのデータを検証のためにドメイン層に送信するというものです。ドメイン層は多くのチェックを行う必要があり、いずれかのチェックが失敗した場合、プレゼンテーション層にそれを通知する必要があります。しかし、プレゼンテーションとドメインの分離では、ドメイン層がプレゼンテーション層と直接通信することはできません。

通知は、ドメイン層が検証中のエラーに関する情報を収集するために使用するオブジェクトです。エラーが発生すると、通知はプレゼンテーション層に送り返され、プレゼンテーション層はエラーに関する詳細情報を表示できます。

仕組み

最も単純な形式では、通知はドメイン層が作業中に生成するエラーメッセージである文字列のコレクションに過ぎません。ドメイン層が行う各検証において、各失敗は通知に追加されるエラーという結果になります。ただし、通知にこれよりも明示的なインターフェースを与えることは理にかなっています。通知オブジェクトには、通常、文字列ではなくエラーコードを使用してエラーを追加するためのメソッドと、通知にエラーが存在するかどうかを判断するための便利なメソッドがあります。

データ転送オブジェクト(DTO)を使用している場合は、レイヤースーパータイプのDTOに通知を追加するのが理にかなっています。これにより、すべてのインタラクションで通知をきれいに使用できます。

トランザクションスクリプトを使用するなど、ドメインロジックが比較的単純な場合は、ロジックは通知への直接参照を持つことができます。これにより、エラーを追加する際に簡単に参照できます。ドメインモデルを使用する、より階層化されたシステムでは、通知を参照するのがより問題になる可能性があります。なぜなら、そのようなドメインモデルは、受信DTOなどの可視性を持たないことが多いためです。この場合、ドメインオブジェクトが簡単にアクセスできる何らかのセッションオブジェクトに通知を配置する必要があります。

エラーコードは、プレゼンテーション層とドメイン層で共有されるクラスに存在する必要があります。エラーコードを使用すると、予期されるエラーをより明示的に示すことができ、プレゼンテーション層がエラーメッセージを出力するだけでなく、よりインタラクティブな方法でエラーを表示することが容易になります。単純なドメイン層では、これらのコードをデータ転送オブジェクト(使用している場合)または特定のインタラクションのエラーコードセットに埋め込むだけで十分なことがよくあります。ドメインモデルでは、エラーコードはドメインモデル自体の語彙を中心に設計する必要があります。

エラーは通常、通知で最も必要とされる側面ですが、ドメイン層が呼び出し元に伝えたいその他の情報を通知で渡すことも便利です。これらには、警告(インタラクションを停止するほど深刻ではない問題)やユーザーに表示する情報メッセージが含まれる場合があります。プレゼンテーション層がエラーが存在するかどうかを簡単に判断できるように、これらは通知上で区別する必要があります。

プレゼンテーション層とドメインロジックが異なるプロセスにあるシステムで通知を使用する場合は、通知に回線を介して安全に転送できる情報のみが含まれていることを確認する必要があります。通常、これはそのような通知にドメインオブジェクトへの参照を埋め込むことができないことを意味します。

プレゼンテーション層は、検証からの応答を受信すると、通知をチェックしてエラーがあるかどうかを判断する必要があります。エラーがある場合、通知から情報を引き出して、これらのエラーをユーザーに表示できます。エラーがある場合の選択肢の1つは、ドメイン層が例外を発生させて、プレゼンテーション層が例外処理メカニズムを使用してエラーを処理できるようにすることです。全体的に見て、検証エラーは十分に一般的であるため、これらの場合に例外処理メカニズムを使用する価値はないと感じていますが、それは圧倒的な好みではありません。

使用時期

検証を開始するモジュールに直接依存関係を持つことができないコード層によって検証が行われる場合は常に、通知を使用する必要があります。これは、階層化アーキテクチャではプレゼンテーションとドメインの分離において非常に一般的です。

通知を使用する最も明白な代替手段は、ドメイン層が例外処理を使用してエラーを示すことです。このようなアプローチでは、検証チェックが失敗した場合、ドメイン層は例外をスローします。これに伴う問題は、最初の検証エラーのみが示されることです。特に検証にリモートドメイン層へのラウンドトリップが必要な場合は、すべての検証エラーを表示する方が役立つことがよくあります。

もう1つの代替手段は、ドメイン層が検証エラーのイベントを発生させることです。これにより、複数のエラーにフラグを立てることができます。ただし、各イベントがネットワーク呼び出しになるため、リモートドメイン層には適していません。

例:ウィンドウのエラーチェック(C#)

図1には、保険金請求を判断するために送信する単純なフォームがあります。送信するデータは3つあります。保険証券番号(文字列)、請求の種類(ピックリストからのテキスト)、および事故の日付(DateTime)です。

データが単純であれば、有効性チェックを待ってください。

  • 3つのデータが欠落していないこと(文字列の場合はnullまたは空白)を確認します。
  • 保険証券番号がデータストアに存在することを確認します。
  • 事故の日付が保険証券の開始日以降であることを確認します。

できるだけ多くの情報をユーザーに返したいので、合理的に複数のエラーを検出できる場合は、そうする必要があります。

ドメイン層から説明を始めます。ドメインロジックへの基本的なインターフェースは、サービ スレイヤーにあります。

クラス ClaimService...

  public void RegisterClaim (RegisterClaimDTO claim) {
    RegisterClaim cmd = new RegisterClaim(claim);
    cmd.Run();
  }

このメソッドは、コマンドオブジェクトを作成して実行し、実際の作業を行います。メソッドコールサービ スレイヤーの背後にコマンドオブジェクトをラップすると、サーバーAPIを簡素化し、リモートファサードを構築しやすくなります。

データを転送するために、データ転送オブジェクトを使用します。RegisterClaimDTOには、メインデータが含まれています。

RegisterClaimDTO:DataTransferObject

  private string _policyID;
  private string _Type;
  private DateTime _incidentDate = BLANK_DATE;

  public string PolicyId {
    get { return _policyID; }
    set { _policyID = value; }
  }
  public string Type {
    get { return _Type; }
    set { _Type = value; }
  }
  public DateTime IncidentDate {
    get { return _incidentDate; }
    set { _incidentDate = value; }
  }

DataTransferObjectは、すべてのDTOのレイヤースーパータイプです。これには、インタラクションに付随する通知を作成してアクセスするための一般的なコードが含まれています。

クラス DataTransferObject...

  private Notification _notification = new Notification();
  public Notification Notification {
    get { return _notification; }
    set { _notification = value; }
  }

通知クラスは、ドメイン層でエラーをキャプチャするために使用するものです。本質的には、エラーのコレクションであり、それぞれが文字列の単純なラッパーです。

クラス Notification...

  private IList _errors = new ArrayList();

  public IList Errors {
    get { return _errors; }
    set { _errors = value; }
  }
  public bool HasErrors {
    get {return 0 != Errors.Count;}      
  }

クラス Notification.Error

  private string message;
  public Error(string message) {
    this.message = message;
  }

コマンドのrunメソッドは非常にシンプルです。

クラス RegisterClaim:ServerCommand...

  public RegisterClaim(RegisterClaimDTO claim) : base(claim) {}
  public void Run() {
    Validate();
    if (!notification.HasErrors)
      RegisterClaimInBackendSystems();    
  }

ここでも、レイヤースーパータイプは、DTOを格納し、通知にアクセスするための一部の一般的な機能を提供します。

クラス ServerCommand...

  public ServerCommand(DataTransferObject data){
    this._data = data;
  }
  protected DataTransferObject _data;
  protected Notification notification {
    get {return _data.Notification;}
  }

validateメソッドは、上記で説明した検証を実行します。本質的に、一連の条件チェックを実行し、何かが失敗した場合に通知にエラーを追加するだけです。

クラス RegisterClaim...

  private void Validate() {
    failIfNullOrBlank(Data.PolicyId, RegisterClaimDTO.MISSING_POLICY_NUMBER);
    failIfNullOrBlank(Data.Type, RegisterClaimDTO.MISSING_INCIDENT_TYPE);
    fail (Data.IncidentDate == RegisterClaimDTO.BLANK_DATE, RegisterClaimDTO.MISSING_INCIDENT_DATE);
    if (isNullOrBlank(Data.PolicyId)) return;
    Policy policy = FindPolicy(Data.PolicyId);
    if (policy == null) {
      notification.Errors.Add(RegisterClaimDTO.UNKNOWN_POLICY_NUMBER);
    }
    else {
      fail ((Data.IncidentDate.CompareTo(policy.InceptionDate) < 0), 
            RegisterClaimDTO.DATE_BEFORE_POLICY_START);
    }
  }

これのほとんどで最も複雑なことは、特定の検証チェックは、他のチェックが失敗していない場合にのみ意味があることです。これは、validateメソッドの条件付きロジックにつながります。より現実的なサイズのメソッドでは、それらをより小さなチャンクに分割することが重要です。

検証の一般的なジェネリックビットは、レイヤースーパータイプから抽出(および配置)できます。

protected bool isNullOrBlank(String s) {
  return (s == null || s == "");
}
protected void failIfNullOrBlank (string s, Notification.Error error) {
  fail (isNullOrBlank(s), error);
}
protected void fail(bool condition, Notification.Error error) {
  if (condition) notification.Errors.Add(error);
}

通知の最も単純な形式のエラーは、文字列をエラーメッセージとして使用することです。少なくとも最小限のラッピング、単純なエラークラスの定義、およびDTOでのインタラクションの固定エラーリストの定義を好みます。

クラス RegisterClaimDTO...

  public static Notification.Error MISSING_POLICY_NUMBER = new Notification.Error("Policy number is missing");
  public static Notification.Error UNKNOWN_POLICY_NUMBER = new Notification.Error("This policy number is unknown");
  public static Notification.Error MISSING_INCIDENT_TYPE = new Notification.Error("Incident type is missing");
  public static Notification.Error MISSING_INCIDENT_DATE = new Notification.Error("Incident Date is missing");
  public static Notification.Error DATE_BEFORE_POLICY_START 
    = new Notification.Error("Incident Date is before we started doing this business");

階層間で通信している場合は、エラーにIDフィールドを追加して、エラーが回線を介してシリアル化されているときに比較が正しく機能するようにする必要がある場合があります。

それは、ドメイン層のすべての興味深い動作とほぼ同じです。プレゼンテーションには、自律ビューを使用します。対象の動作は、送信ボタンが押されたときに発生します。

クラス FrmRegisterClaim...

  RegisterClaimDTO claim;
  public void Submit() {
    saveToClaim();
    service.RegisterClaim(claim);
    if (claim.Notification.HasErrors) {
      txtResponse.Text = "Not registered, see errors";
      indicateErrors();
    }
    else txtResponse.Text = "Registration Succeeded";
  }
  private void saveToClaim() {
    claim = new RegisterClaimDTO();
    claim.PolicyId = txtPolicyNumber.Text;
    claim.IncidentDate = pkIncidentDate.Value;
    claim.Type = (string) cmbType.SelectedItem;
  }

このメソッドは、コントロールから情報を引き出してDTOに入力し、データをドメイン層に送信します。返されるDTOにエラーが含まれている場合は、それらを表示する必要があります。

クラス FrmRegisterClaim...

  private void indicateErrors() {
    checkError(RegisterClaimDTO.MISSING_POLICY_NUMBER, txtPolicyNumber);
    checkError(RegisterClaimDTO.MISSING_INCIDENT_TYPE, cmbType);
    checkError(RegisterClaimDTO.DATE_BEFORE_POLICY_START, pkIncidentDate);
    checkError(RegisterClaimDTO.MISSING_INCIDENT_DATE, pkIncidentDate);
    checkError(RegisterClaimDTO.DATE_BEFORE_POLICY_START, pkIncidentDate);
  }
  private void checkError (Notification.Error error, Control control) {
    if (claim.Notification.IncludesError(error)) 
      showError(control, error.ToString());
  }

ここでは、DTOで定義されているエラーを使用して、それらをフォームのフィールドにマッピングしているため、正しいフィールドに正しいエラーが表示されます。

エラーを実際に表示するには、.NETに付属する標準のエラープロバイダーを使用します。これは、問題のあるフィールドの横にエラーアイコンと、問題の原因となっているエラーメッセージを表示するツールチップを表示します。

クラス FrmRegisterClaim...

  private ErrorProvider errorProvider = new ErrorProvider();
  void showError (Control arg, string message) {
    errorProvider.SetError(arg, message);
  }

フィールドの内容が変更された場合は、エラー情報をクリアします。

クラス FrmRegisterClaim...

  void clearError (Control arg) {
    errorProvider.SetError(arg, null);
  }
  private void txtPolicyNumber_TextChanged(object sender, EventArgs e) {
    clearError((Control)sender);
  }
  private void cmbType_TextChanged(object sender, EventArgs e) {
    clearError((Control)sender);
  }
  private void pkIncidentDate_ValueChanged(object sender, EventArgs e) {
    clearError((Control)sender);    
  }