도메인에서 리포지토리에 액세스


14

작업 기록 시스템이 있다고 가정하면, 작업이 기록 될 때 사용자는 범주를 지정하고 기본적으로 작업 상태는 'Outstanding'입니다. 이 경우 카테고리 및 상태가 엔티티로 구현되어야한다고 가정하십시오. 일반적으로 나는 이것을 할 것입니다 :

응용 프로그램 계층 :

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를 작성하는 것이 어렵고, 태스크 엔티티에 따라 자동 테스트를 작성하는 것이 어렵고, 이제 모든 태스크 엔티티가 리포지토리에 대한 두 가지 참조를 유지하십시오. 물론, 그것은 당신의 경우에 견딜 수 있습니다. 반면에 TaskUpdater올바른 리포지토리에 대한 참조를 유지 하는 별도의 유틸리티 클래스 를 만드는 것이 종종 또는 적어도 때때로 더 나은 솔루션 일 수 있습니다.

중요한 부분은 : TaskUpdater여전히 도메인 계층의 일부가 될 것입니다! 업데이트 또는 생성 코드를 별도의 클래스에 넣었다고해서 다른 레이어로 전환해야하는 것은 아닙니다.


나는 이것이 정적이 아닌 메소드에 적용되는 것을 보여주기 위해 편집했다. 나는 팩토리 메소드가 엔티티의 일부가 아니라고 생각하지 않았습니다.
Paul T Davies

@PaulTDavies : 내 편집 참조
Doc Brown

나는 당신이 여기서 말하는 것에 동의하지만, 나는 비즈니스 규칙 이라는 요점을 나타내는 간결한 부분을 추가 할 것 Status = _statusRepository.GetById(Constants.Status.OutstandingId)입니다. "비즈니스는 모든 작업의 ​​초기 상태가 두드러 질 것입니다"라고 읽을 수 있습니다. 해당 코드 행은 CRUD 조작을 통한 데이터 관리에만 관심이있는 저장소 내에 속하지 않습니다.
지미 호파

@ JimimHoffa : 흠, 여기 아무도 그런 종류의 라인을 저장소 클래스 중 하나에 넣을 것을 제안하지 않았습니다 .OP도 나도 아닙니다.
Doc Brown

저는 TaskUpdater가 domian 서비스라는 아이디어를 매우 좋아합니다. DDD 원칙을 유지하기 위해 약간의 퍼지처럼 보이지만 Task를 사용할 때마다 리포지토리를 주입하지 않아도됩니다.
Paul T Davies

6

귀하의 상태 예제가 실제 코드인지 또는 여기에 데모 목적인지 모르겠지만 ID가 상수로 정의되면 상태를 엔티티 (Aggregate Root를 언급하지 않음)로 구현해야한다는 것이 이상합니다. 코드에서- Constants.Status.OutstandingId. 데이터베이스에서 원하는만큼 추가 할 수있는 "동적"상태의 목적을 무시하지 않습니까?

나는 당신의 경우에 Task(필요한 경우 StatusRepository에서 올바른 상태를 얻는 작업을 포함 하여) 건설 하는 TaskFactory것이 Task사소한 객체의 조립이 아니기 때문에 그 자체로 머무르기보다는 가치가 있다고 덧붙입니다.

그러나 :

엔터티가 리포지토리에 액세스하면 안된다는 말을 지속적으로 들었습니다.

이 진술은 부정확하고 지나치게 단순하며 잘못되고 위험합니다.

도메인 기반 아키텍처에서는 엔티티가 자체 저장 방법을 몰라 야한다는 것이 일반적으로 받아 들여지고 있습니다. 이것이 지속성 무지 원칙입니다. 따라서 저장소에 자신을 추가하기 위해 저장소에 대한 호출이 없습니다. 다른 엔티티저장 하는 방법과시기를 알아야 합니까? 다시 말하지만, 그 책임은 다른 객체, 즉 애플리케이션 계층 서비스와 같은 현재 사용 사례의 실행 컨텍스트 및 전체 진행 상황을 알고있는 객체에 속하는 것으로 보입니다.

엔티티가 저장소를 사용하여 다른 엔티티검색 할 수 있습니까? 필요한 개체는 일반적으로 집계의 범위에 있거나 다른 개체를 통과하여 얻을 수 있으므로 시간의 90 %가 필요하지 않습니다. 그러나 그렇지 않은 경우가 있습니다. 예를 들어 계층 구조를 취하는 경우 엔터티는 종종 고유 한 동작의 일부로 모든 조상, 특정 손자 등에 액세스해야합니다. 그들은이 먼 친척들에 대한 직접적인 언급이 없습니다. 이러한 친척들을 수술의 매개 변수로 그들에게 전달하는 것은 불편할 것입니다. 그렇다면 리포지토리를 사용하여 가져 오는 것이 어떻습니까?

다른 몇 가지 예가 있습니다. 문제는 때로는 기존 엔터티에 완벽하게 들어 맞기 때문에 도메인 서비스에 배치 할 수없는 동작이 있다는 것입니다. 그러나이 엔티티는 저장소에 액세스하여 전달할 수없는 루트 또는 루트 콜렉션을 수화해야합니다.

엔터티에서 저장소에 접근하는 것은 나쁘지 않다 그래서 그 자체로 , 서로 다른을 형성의에서 그 결과 걸릴 수 있습니다 다양한 재앙에서 허용에 이르기까지 설계 결정을.


엔티티가 이미 관계가있는 엔티티에 액세스하기 위해 저장소를 사용해야한다는 데 동의하지 않습니다. 개체 그래프를 통과하여 해당 엔티티에 액세스 할 수 있어야합니다. 이런 식으로 리포지토리를 사용하는 것은 절대 아닙니다. 내가 여기서 논의하고있는 것은 그 실체가 아직 언급하지 않았지만 어떤 비즈니스 조건 하에서 하나를 생성해야한다는 것이다.
Paul T Davies

글쎄, 당신이 나를 잘 읽었다면, 우리는 그것에 완전히 동의합니다 ...
guillaume31

2

이것이 내 도메인 내에서 Enum 또는 순수 조회 테이블을 사용하지 않는 이유 중 하나입니다. 긴급 및 상태는 모두 상태이며 상태에 직접 속한 상태와 관련된 논리가 있습니다 (예 : 현재 상태에서 어떤 상태로 전환 할 수 있는지). 또한 상태를 순수한 값으로 기록하면 작업이 주어진 상태에 있었던 시간과 같은 정보가 손실됩니다. 나는 상태를 클래스 계층 구조로 나타냅니다. (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 Domain Objects가 아닌 의미있는 작업을 제공하는 Domain Driven Design의 일부입니다.

  3. 여기에 표시하지 않지만 작업은 현재 상태를 저장하는 방법에 대한 세부 정보를 숨길 것입니다. 나는 일반적으로 관심있는 경우 대중이 쿼리 할 수 ​​있도록 보호 된 HistoricalStates 목록을 가지고 있습니다. 그렇지 않으면 HistoricalStates.Single (state.Duration.End == null)을 쿼리하는 getter로 현재 상태를 노출합니다.

  4. TransitionTo 함수는 전환에 유효한 상태에 대한 논리를 포함 할 수 있으므로 중요합니다. 열거 형이 있다면 그 논리는 다른 곳에 있어야합니다.

DDD 접근 방식을 조금 더 이해하는 데 도움이되기를 바랍니다.


1
다른 상태가 상태 패턴 예제에서와 같이 다른 동작을 갖는 경우에는 이것이 올바른 접근법 일 것입니다. 또한 논의 된 문제도 확실히 해결합니다. 그러나 다른 행동이 아닌 다른 가치를 가진다면 각 주에 대한 수업을 정당화하는 것이 어렵다는 것을 알았습니다.
Paul T Davies

1

나는 언젠가 같은 문제를 해결하려고 노력했지만 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;
        }

    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.