DDD에서 저장소가 엔티티 또는 도메인 오브젝트를 노출해야합니까?


11

내가 이해했듯이 DDD에서는 집계 루트와 함께 저장소 패턴을 사용하는 것이 적절합니다. 내 질문은 데이터를 엔티티 또는 도메인 객체 / DTO로 반환해야합니까?

어쩌면 일부 코드는 내 질문을 더 설명 할 것입니다.

실재

public class Customer
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

이런 식으로해야합니까?

public Customer GetCustomerByName(string name) { /*some code*/ }

아니면 이런거?

public class CustomerDTO
{
  public Guid Id { get; set; }
  public FullName { get; set; }
}

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

추가 질문 :

  1. 저장소에서 IQueryable 또는 IEnumerable을 반환해야합니까?
  2. 서비스 또는 저장소에서 내가 좋아하는 뭔가를해야 ... GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? 또는 그냥 비슷한 방법을 만드 GetCustomerBy(Func<string, bool> predicate)십니까?

무엇을 것입니다 GetCustomerByName('John Smith')데이터베이스에 당신이 스무 존 스미스가있는 경우 반환? 두 사람이 같은 이름을 가지고 있지 않다고 가정합니다.
bdsl

답변:


8

엔티티 또는 도메인 객체 / DTO로 데이터를 반환해야합니까?

글쎄 그것은 전적으로 사용 사례에 달려 있습니다. 전체 엔티티 대신 DTO를 반환한다고 생각할 수있는 유일한 이유는 엔티티가 거대하고 일부만 처리해야하기 때문입니다.

이 경우 도메인 모델을 재고하고 큰 엔터티를 관련 작은 엔터티로 분할해야합니다.

  1. 저장소에서 IQueryable 또는 IEnumerable을 반환해야합니까?

경험상 가장 간단한 규칙은 항상 가장 단순한 (상속 계층 구조에서) 가능한 유형을 반환하는 것입니다. 따라서 IEnumerable리포지토리 소비자가 IQueryable.

개인적으로,을 반환하는 IQueryable것은 새는 추상화 라고 생각 하지만 열정적이지 않다고 주장하는 여러 개발자를 만났습니다. 내 생각에 모든 쿼리 로직은 리포지토리에 포함되어 있어야합니다. 호출 코드가 쿼리를 사용자 정의하도록 허용하면 저장소의 요점은 무엇입니까?

  1. 서비스 또는 저장소에서 다음과 같이해야합니다 .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? 또는 GetCustomerBy (Func predicate)와 같은 메소드를 작성 하시겠습니까?

포인트 1에서 언급 한 것과 같은 이유로 반드시를 사용 하지 마십시오 GetCustomerBy(Func<string, bool> predicate). 처음에는 유혹적인 것처럼 보일 수 있지만 이것이 바로 사람들이 일반 리포지토리를 싫어하는 것을 배운 이유입니다. 새는 군

같은 GetByPredicate(Func<T, bool> predicate)것은 구체적인 수업 뒤에 숨겨져있을 때만 유용합니다. 따라서 구체적인 리포지토리 (예 :) 만 사용 RepositoryBase<T>하는 노출 된 추상 기본 클래스 가 있다면 괜찮을 것입니다.protected T GetByPredicate(Func<T, bool> predicate)public class CustomerRepository : RepositoryBase<Customer>


그렇다면 도메인 계층에 DTO 클래스가 있다고해도 괜찮습니까?
codefish

그건 내가 말하려는 것이 아닙니다. 나는 DDD 전문가가 아니기 때문에 그것이 용납 될 수 있는지 말할 수 없습니다. 호기심으로, 당신은 전체 실체를 반환하지 않았습니까? 너무 큽니까?
MetaFight

아뇨 나는 옳고 수용 가능한 일이 무엇인지 알고 싶습니다. 전체 엔터티 또는 하위 집합 만 반환합니까? 나는 그것이 당신의 대답에 달려 있다고 생각합니다.
codefish

6

CQRS를 사용하여 도메인을 구현하는 상당한 규모의 커뮤니티가 있습니다. 내 저장소의 인터페이스가 사용 된 모범 사례와 유사하다면 너무 길을 잃지 않을 것이라고 생각합니다.

내가 본 것을 바탕으로 ...

1) 명령 핸들러는 일반적으로 저장소를 사용하여 저장소를 통해 집계를로드합니다. 명령은 집계의 단일 특정 인스턴스를 대상으로합니다. 저장소는 ID별로 루트를로드합니다. 내가 볼 수있는 것은 아니지만 집계 컬렉션에 대해 명령이 실행되는 경우는 없습니다 (대신 먼저 집계를 수집하기 위해 쿼리를 실행 한 다음 컬렉션을 열거하고 각각에 명령을 발행합니다).

따라서 집계를 수정하려는 컨텍스트에서 저장소가 엔티티 (일명 집계 루트)를 반환 할 것으로 기대합니다.

2) 쿼리 핸들러는 집계를 전혀 건드리지 않습니다. 그 대신, 특정 시점에서 집계 / 집계의 상태를 설명하는 가치 개체 인 집계의 투영을 처리합니다. 따라서 AggregateDTO가 아니라 ProjectionDTO를 생각하면 올바른 아이디어가 있습니다.

집계에 대해 쿼리를 실행하고 표시를 준비하는 등의 컨텍스트에서 엔터티가 아닌 DTO 또는 DTO 컬렉션이 반환 될 것으로 기대합니다.

귀하의 모든 getCustomerByProperty전화는 나에게 질문처럼 보이므로 후자의 범주에 속합니다. 컬렉션을 생성하기 위해 단일 진입 점을 사용하고 싶을 수도 있으므로

getCustomersThatSatisfy(Specification spec)

합리적인 선택입니다. 그런 다음 쿼리 핸들러는 제공된 매개 변수에서 적절한 사양을 구성하고 해당 사양을 리포지토리에 전달합니다. 단점은 서명이 리포지토리가 메모리의 컬렉션임을 실제로 제안한다는 것입니다. 저장소가 관계형 데이터베이스에 대해 SQL 문을 실행하는 추상화 일 경우 술어가 많은 것을 구매한다는 것은 분명하지 않습니다.

그래도 도움이 될 수있는 몇 가지 패턴이 있습니다. 예를 들어, 사양을 직접 작성하는 대신, 제한 조건에 대한 설명을 저장소에 전달하고 저장소 구현이 수행 할 조치를 결정하도록 허용하십시오.

경고 : 입력과 같은 자바 감지

interface CustomerRepository {
    interface ConstraintBuilder {
        void setLastName();
        void setFirstName();
    }

    interface ConstraintDescriptor {
        void copyTo(ConstraintBuilder builder);
    }

    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}

SQLBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        WhereClauseBuilder builder = new WhereClauseBuilder();
        descriptor.copyTo(builder);
        Query q = createQuery(builder.build());
        //...
     }
}

CollectionBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        PredicateBuilder builder = new PredicateBuilder();
        descriptor.copyTo(builder);
        Predicate p = builder.build();
        // ...
}

class MatchLastName implements CustomerRepository.ConstraintDescriptor {
    private final lastName;
    // ...

    void copyTo(CustomerRepository.ConstraintBuilder builder) {
        builder.setLastName(this.lastName);
    }
}

결론 : 집합체 제공과 DTO 제공 사이의 선택은 소비자가 소비자와 함께 할 것으로 기대하는 것에 달려 있습니다. 내 생각에는 각 컨텍스트에 대한 인터페이스를 지원하는 하나의 구체적인 구현 일 것입니다.


그것은 모두 훌륭하고 훌륭하지만 asker는 CQRS 사용에 대해 언급하지 않습니다. 당신이 옳아 요, 만약 그가했다면 그의 문제는 존재하지 않을 것입니다.
MetaFight

나는 CQRS에 대해 전혀 모른다. 이해했듯이 AggregateDTO 또는 ProjectionDTO 인 경우 반환 할 항목을 생각할 때 ProjectionDTO를 반환합니다. 그런 다음 getCustomersThatSatisfy(Specification spec)검색 옵션에 필요한 속성을 나열하겠습니다. 내가 올바르게 받고 있습니까?
codefish

불분명합니다. AggregateDTO가 존재해야한다고 생각하지 않습니다. 집계의 요점은 모든 변경이 비즈니스 불변을 충족시키는 것입니다. 충족해야하는 도메인 규칙을 캡슐화 한 것입니다. 즉, 동작입니다. 반면에 프로젝션은 비즈니스에 수용 가능한 상태의 일부 스냅 샷을 나타냅니다. 사양을 명확히하려면 편집을 참조하십시오.
VoiceOfUnreason

AggregateDTO가 존재하지 않아야한다는 VoiceOfUnreason에 동의합니다. ProjectionDTO를 데이터의 뷰 모델로 생각합니다. 필요에 따라 다양한 소스에서 데이터를 가져와 발신자에게 필요한 모양에 맞게 모양을 조정할 수 있습니다. 집계 루트는 모든 관련 데이터를 완벽하게 표현해야 저장하기 전에 모든 참조 규칙을 테스트 할 수 있습니다. DB 테이블 변경 또는 수정 된 데이터 관계와 같이보다 과감한 환경에서만 형태가 변경됩니다.
Brad Irby

1

DDD에 대한 나의 지식을 바탕으로

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

저장소에서 IQueryable 또는 IEnumerable을 반환해야합니까?

이 질문에 대해 다른 사람들의 믹스 뷰를 찾을 수 있습니다. 그러나 개인적으로 CustomerService와 같은 서비스에서 저장소를 사용하는 경우 IQueryable을 사용하고 그렇지 않으면 IEnumerable을 사용할 수 있다고 생각합니다.

리포지토리가 IQueryable을 반환해야합니까?

서비스 또는 저장소에서 다음과 같이해야합니다 .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? 또는 GetCustomerBy (Func predicate)와 같은 메소드를 작성 하시겠습니까?

리포지토리에는 말한 것처럼 일반적인 기능이 있어야 GetCustomer(Func predicate)하지만 서비스 계층에는 세 가지 다른 방법을 추가하십시오. 서비스가 다른 클라이언트에서 호출 될 가능성이 있고 다른 DTO가 필요하기 때문입니다.

아직 모르는 경우 일반적인 CRUD에 일반 리포지토리 패턴 을 사용할 수도 있습니다 .

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