web-dev-qa-db-ja.com

ドメインからリポジトリにアクセスする

タスクロギングシステムがあるとしましょう。タスクがログに記録されると、ユーザーはカテゴリを指定し、タスクはデフォルトで「未解決」のステータスになります。この例では、カテゴリとステータスをエンティティとして実装する必要があると想定しています。通常、私はこれを行います:

アプリケーション層:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

エンティティ:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

エンティティはリポジトリにアクセスしてはいけないと常に言われているので、私はこのようにしていますが、これを行うと、私にとってははるかに理にかなっています。

エンティティ:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

いずれにしても、ステータスリポジトリには依存関係が挿入されているため、実際の依存関係はありません。これは、タスクのデフォルトが未解決であるという決定を下しているドメインであることを考えると、私にはさらに感じられます。以前のバージョンは、それがその決定を行うアプリケーションレイヤーであるように感じます。これが可能ではないはずなのに、なぜリポジトリ契約がドメイン内によくあるのですか?

これはより極端な例であり、ここではドメインが緊急度を決定します。

エンティティ:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

可能なすべてのバージョンの緊急性を渡す方法はなく、アプリケーションレイヤーでこのビジネスロジックを計算する方法もないので、これが最も適切な方法でしょうか。

これはドメインからリポジトリにアクセスする正当な理由ですか?

編集:これは非静的メソッドの場合にも当てはまる:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}
15
Paul T Davies

あなたは混ざっている

エンティティはリポジトリにアクセスしないでください

(これは良い提案です)

そして

ドメイン層はリポジトリにアクセスすべきではありません

(リポジトリがアプリケーション層ではなくドメイン層の一部である限り、これは不適切な提案になる可能性があります)。実際には、どのエンティティにも属さないstaticメソッドを使用しているため、例ではエンティティがリポジトリにアクセスするケースは示されていません。

その作成ロジックをエンティティークラスの静的メソッドに入れたくない場合は、(ドメインレイヤーの一部として)別のファクトリークラスを導入して、そこに作成ロジックを置くことができます。

編集:あなたのUpdateの例:与えられた_urgencyRepositorystatusRepositoryは、クラスTaskのメンバーであり、なんらかのインターフェースとして定義されています。Taskを今すぐ使用できるようにする前に、それらをUpdateエンティティに注入する必要があります(たとえば、タスクコンストラクターで)。または、それらを静的メンバーとして定義しますが、マルチスレッドの問題を簡単に引き起こしたり、異なるタスクエンティティに対して異なるリポジトリを同時に必要とする場合に問題を引き起こす可能性があることに注意してください。

この設計により、Taskエンティティを個別に作成することが少し難しくなるため、Taskエンティティの単体テストを作成することが難しくなり、タスクエンティティに応じて自動テストを作成することが難しくなり、すべてのタスクエンティティがメモリオーバーヘッドをもう少し生成します。リポジトリへのその2つの参照を保持する必要があります。もちろん、それはあなたのケースでは許容できるかもしれません。一方、適切なリポジトリへの参照を保持する別のユーティリティクラスTaskUpdaterを作成することは、多くの場合、または少なくとも場合によっては、より優れたソリューションになる可能性があります。

重要な部分は次のとおりです。TaskUpdaterは引き続きドメインレイヤーの一部になります。更新コードまたは作成コードを別のクラスに配置したからといって、別のレイヤーに切り替える必要があるわけではありません。

8
Doc Brown

ステータスの例が実際のコードであるのか、ここではデモンストレーションのためだけなのかわかりませんが、IDが定義された定数である場合、Statusをエンティティ(集約ルートは言うまでもなく)として実装する必要があるのは奇妙に思えますコード内-Constants.Status.OutstandingId。これは、データベースに必要なだけ追加できる「動的」ステータスの目的に反しませんか?

あなたの場合、Taskの構築(必要に応じてStatusRepositoryから正しいステータスを取得するジョブを含む)は、TaskFactory自体に留まるのではなく、Taskに値するかもしれません。オブジェクト。

だが :

エンティティはリポジトリにアクセスするべきではないと常に言われています

このステートメントは、不正確であり、せいぜい単純化しており、誤解を招き、最悪の場合は危険です。

エンティティが自体を格納する方法を知ってはならないことは、ドメイン主導のアーキテクチャではかなり一般的に受け入れられています(永続化の無知の原則です)。そのため、自分自身をリポジトリに追加するためにそのリポジトリを呼び出す必要はありません。他のエンティティを格納する方法(およびいつ)を知っている必要がありますか?繰り返しますが、その責任は別のオブジェクトに属しているようです-多分、アプリケーションレイヤーサービスのように、実行コンテキストと現在のユースケースの全体的な進行状況を認識しているオブジェクトです。

エンティティはリポジトリを使用して別のエンティティを取得できますか?必要なエンティティは通常、その集計のスコープ内にあるか、他のオブジェクトのトラバースによって取得できるため、必要のない時間の90%。しかし、そうでない場合もあります。たとえば、階層構造をとる場合、エンティティは多くの場合、その固有の動作の一部として、すべての祖先や特定の孫などにアクセスする必要があります。彼らはこれらのリモートの親戚への直接の参照を持っていません。これらの親族を操作のパラメータとして渡すのは不便です。それで、それらを取得するためにリポジトリを使用しないのはなぜですか?

他にもいくつか例があります。問題は、既存のエンティティに完全に適合しているように見えることがあるため、ドメインサービスに配置できない動作があることです。しかし、このエンティティは、ルートまたはルートに渡すことができないルートのコレクションをハイドレートするためにリポジトリにアクセスする必要があります。

したがって、エンティティからリポジトリにアクセスすること自体は悪いことではありません、それはから生じるさまざまな形式をとることができます破滅的なものから許容できるものまで、さまざまな設計上の決定

6
guillaume31

これが、ドメイン内でEnumまたは純粋なルックアップテーブルを使用しない理由の1つです。緊急度とステータスはどちらも状態であり、状態に直接属する状態に関連付けられたロジックがあります(たとえば、現在の状態が与えられたときにどの状態に遷移できるかなど)。また、状態を純粋な値として記録すると、タスクが特定の状態にあった時間などの情報が失われます。ステータスをクラス階層として表現しています。 (C#の場合)

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

CompletedTaskStatusの実装はほとんど同じです。

ここで注意すべき点がいくつかあります。

  1. デフォルトのコンストラクタを保護します。これは、永続性からオブジェクトをプルするときにフレームワークがそれを呼び出すことができるようにするためです(EntityFramework Code-firstとNHibernateの両方が、ドメインオブジェクトから派生したプロキシを使用して魔法をかけます)。

  2. プロパティセッターの多くは、同じ理由で保護されています。インターバルの終了日を変更したい場合は、Interval.End()関数を呼び出す必要があります(これはドメイン主導設計の一部であり、Anemicドメインオブジェクトではなく、意味のある操作を提供します。

  3. ここには表示しませんが、タスクは現在のステータスを保存する方法の詳細も同様に非表示にします。私は通常、HistoricalStatesの保護されたリストを持っているので、興味がある場合は一般に問い合わせることができます。それ以外の場合は、HistoricalStates.Single(state.Duration.End == null)をクエリするゲッターとして現在の状態を公開します。

  4. TransitionTo関数は、遷移に有効な状態に関するロジックを含むことができるため、重要です。列挙型がある場合、そのロジックは別の場所にある必要があります。

うまくいけば、これはDDDのアプローチを少しよく理解するのに役立ちます。

2
Michael Brown

私はしばらくの間同じ問題を解決しようとしましたが、私はそのようにTask.UpdateTask()を呼び出せるようにしたいと思いましたが、私はむしろそれはドメイン固有になるので、あなたの場合はTask.ChangeCategoryと呼ぶかもしれません(...)CRUDだけでなく、アクションを示します。

とにかく、私はあなたの問題を試し、これを思いつきました...私のケーキを持って、それも食べてください。アクションはエンティティで実行されますが、すべての依存関係の注入は行われません。代わりに、静的メソッドで作業が行われるため、エンティティの状態にアクセスできます。ファクトリーはすべてをまとめ、通常、エンティティーが実行する必要がある作業を実行するために必要なすべてのものを備えています。クライアントコードは見た目もすっきりとしており、エンティティはリポジトリインジェクションに依存していません。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
1
Mike