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


14

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

アプリケーション層:

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;
    }
}

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

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

編集:これは、非静的メソッドの場合もあります。

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;
    }
}

回答:


8

あなたは混ざっている

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

(これは良い提案です)

そして

ドメイン層はリポジトリにアクセスしないでください

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

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

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

この設計により、Taskエンティティを分離して作成するのが少し難しくなります。したがって、Taskエンティティの単体テストを書くのが難しくなり、タスクエンティティに応じて自動テストを書くのが難しくなります。リポジトリへの2つの参照を保持します。もちろん、それはあなたの場合には耐えられるかもしれません。一方、TaskUpdater適切なリポジトリへの参照を保持する別のユーティリティクラスを作成することは、多くの場合、または少なくとも時にはより良いソリューションです。

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


私は、これが静的メソッドと同様に非静的メソッドにも適用されることを示すために編集しました。ファクトリメソッドがエンティティの一部ではないことを本当に考えたことはありません。
ポールTデイヴィス

@PaulTDavies:私の編集を参照してください
ドク・ブラウン

私はあなたがここで言っていることに同意しますStatus = _statusRepository.GetById(Constants.Status.OutstandingId)が、ビジネスルールである点を描く簡潔な部分を追加します。あなたは「ビジネスはすべてのタスクの初期ステータスを決定する」と読むことができます。そのコード行はリポジトリ内に属していません。リポジトリの唯一の懸念はCRUD操作によるデータ管理です。
ジミー・ホッファ

@JimmyHoffa:えーと、この種の行をリポジトリクラスの1つ(OPでも私でもない)に入れることを提案している人はいませんでした。
Doc Brown

ドミノサービスとしてのTaskUpdaterのアイデアはとても気に入っています。どういうわけか、DDDの原則を保持するためだけのちょっとしたファッジのように思えますが、それは、Taskを使用するたびにリポジトリを挿入することを回避できることを意味します。
ポールTデイヴィス

6

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

あなたの場合、オブジェクトの重要な集合体であるため、Task(必要に応じてStatusRepositoryから適切なステータスを取得するジョブを含む)の構築はTaskFactoryTaskそれ自体にとどまるよりもむしろ価値があるかもしれないと付け加えます。

だが :

エンティティがリポジトリにアクセスしてはならないと一貫して言われています

この記述は、最高で不正確で単純化し過ぎており、誤解を招き、最悪の場合は危険です。

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

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

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

そのため、エンティティからリポジトリにアクセスすること自体は悪くありません。壊滅的なものから受け入れられるものまで、さまざまな設計上の決定から生じるさまざまな形をとることができます。


エンティティがリポジトリを使用して、既に関係があるエンティティにアクセスする必要があることに同意しません。オブジェクトグラフを走査して、そのエンティティにアクセスできる必要があります。この方法でリポジトリを使用することは絶対にありません。ここで説明しているのは、エンティティがまだ参照していないが、何らかのビジネス条件下でエンティティを作成する必要があるということです。
ポールTデイヴィス

あなたが私をよく読んだなら、私たちはそれに完全に同意します
...-guillaume31

2

これが、私のドメイン内で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のアプローチを少しよく理解するのに役立つことを願っています。


1
状態パターンの例のように、異なる状態の動作が異なる場合、これは確かに正しいアプローチであり、議論された問題も確実に解決します。ただし、各状態の動作が異なるのではなく、値が異なる場合、各状態のクラスを正当化することは困難です。
ポールTデイヴィス

1

私はしばらくの間同じ問題を解決しようとしてきたので、そのようにTask.UpdateTask()を呼び出すことができるようにしたいと決めました。 (...)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;
        }

    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.