DDD-엔티티가 리포지토리에 직접 액세스 할 수없는 규칙


185

도메인 기반 디자인에서있을 것 같습니다 제비계약 이 엔티티해야하지 액세스 저장소 직접.

이것은 Eric Evans Domain Driven Design 서적 에서 나왔습니까 아니면 다른 곳에서 나왔습니까?

그 뒤에 추론에 대한 좋은 설명이 어디에 있습니까?

편집 : 명확히하기 위해 : 비즈니스 논리와 별도의 계층으로 데이터 액세스를 분리하는 고전적인 OO 관행에 대해 이야기하고 있지 않습니다 .DDD에서 엔티티가 데이터와 대화하지 않아야하는 특정 배열에 대해 이야기하고 있습니다. 액세스 계층 전혀 없음 (즉, 리포지토리 객체에 대한 참조를 보유하지 않아야 함)

업데이트 : 나는 그의 대답이 가장 가깝게 보였기 때문에 BacceSR에 현상금을 주었지만 여전히 이것에 대해 어두운 상태에 있습니다. 중요한 원칙이라면 온라인 어딘가에 좋은 기사가 있어야합니까?

업데이트 : 2013 년 3 월, 질문에 대한 지지자들은 이것에 많은 관심이 있음을 암시하며 많은 답변이 있었음에도 불구하고 사람들이 이것에 대한 아이디어를 가지고 있다면 더 많은 여지가 있다고 생각합니다.


내 질문 stackoverflow.com/q/8269784/235715을 살펴보십시오 .Entity가 저장소에 액세스하지 않고 논리를 캡처하기 어려운 상황을 보여줍니다. 엔티티가 저장소에 액세스 할 수 없어야한다고 생각하지만 저장소 참조없이 코드를 다시 작성할 수 있지만 현재는 생각할 수없는 상황에 대한 해결책이 있습니다.
Alex Burtsev

어디에서 왔는지 모릅니다. 내 생각 : 나는이 오해가 사람들로부터 DDD가 무엇인지 이해하지 못한다고 생각합니다. 이 방법은 소프트웨어를 구현하기위한 것이 아니라 소프트웨어를 디자인하기위한 것입니다 (도메인 .. 디자인). 당시에는 건축가와 구현자가 있었지만 이제는 소프트웨어 개발자 만 있습니다. DDD는 건축가를위한 것입니다. 그리고 설계자가 소프트웨어를 설계 할 때는 준비된 설계를 구현할 개발자를위한 메모리 나 데이터베이스를 나타내는 도구 나 패턴이 필요합니다. 그러나 비즈니스 측면에서 볼 때 디자인 자체에는 리포지토리가 필요하지 않습니다.
berhalak

답변:


47

여기에 약간의 혼란이 있습니다. 리포지토리는 집계 루트에 액세스합니다. 집계 루트는 엔티티입니다. 그 이유는 우려와 분리의 분리 때문입니다. 소규모 프로젝트에서는 이치에 맞지 않지만 대규모 팀의 경우 "제품 리포지토리를 통해 제품에 액세스합니다. 제품은 ProductCatalog 개체를 포함하여 엔터티 모음의 총체입니다. ProductCatalog를 업데이트하려면 ProductRepository를 거쳐야합니다. "

이러한 방식으로 비즈니스 로직과 사물이 업데이트되는 위치를 매우 명확하게 분리 할 수 ​​있습니다. 혼자서 아이를 낳지 않고 복잡한 모든 일을 제품 카탈로그에 작성하는이 전체 프로그램을 작성하고, 업스트림 프로젝트에 통합 할 때이를보고 깨닫고 있습니다. 다 버려야합니다. 또한 사람들이 팀에 합류하고 새로운 기능을 추가 할 때 갈 곳과 프로그램 구성 방법을 알고 있습니다.

하지만 기다려! 리포지토리는 리포지토리 패턴에서와 같이 지속성 계층을 나타냅니다. 더 나은 세상에서 Eric Evans의 Repository와 Repository Pattern은 이름이 약간 겹치므로 별개의 이름을 갖습니다. 저장소 패턴을 얻으려면 서비스 버스 또는 이벤트 모델 시스템을 사용하여 데이터에 액세스하는 다른 방법과 대조됩니다. 일반적으로이 레벨에 도달하면 Eric Evans의 저장소 정의가 나란히 진행되고 경계 컨텍스트에 대해 이야기하기 시작합니다. 각각의 경계 컨텍스트는 본질적으로 자체 응용 프로그램입니다. 제품 카탈로그로 물건을 가져 오기위한 정교한 승인 시스템이있을 수 있습니다. 오리지널 디자인에서 제품은 중심 제품이지만이 제한된 맥락에서 제품 카탈로그가 있습니다. 여전히 서비스 버스를 통해 제품 정보에 액세스하고 제품을 업데이트 할 수 있습니다.

원래 질문으로 돌아 가기 엔터티 내에서 리포지토리에 액세스하는 경우 엔터티가 실제로 비즈니스 엔터티가 아니라 서비스 계층에 존재해야하는 항목임을 의미합니다. 엔터티는 비즈니스 개체이므로 가능한 한 DSL (도메인 특정 언어)과 비슷해야합니다. 이 계층에는 비즈니스 정보 만 있습니다. 성능 문제를 해결하는 경우 비즈니스 정보 만 있어야하므로 다른 곳을 찾아야합니다. 갑자기 응용 프로그램 문제가 발생하면 응용 프로그램을 확장 및 유지 관리하기가 매우 어려워집니다. 실제로 DDD의 핵심은 유지 관리 가능한 소프트웨어 만들기입니다.

의견에 대한 답변 1 : 옳고 좋은 질문입니다. 따라서 모든 유효성 검사가 도메인 계층에서 발생하는 것은 아닙니다 . Sharp는 원하는 것을 수행하는 "DomainSignature"속성을 가지고 있습니다. 지속성을 인식하고 있지만 속성은 도메인 계층을 깨끗하게 유지합니다. 예를 들어 동일한 이름을 가진 중복 엔티티가 없는지 확인합니다.

그러나 더 복잡한 유효성 검사 규칙에 대해 이야기합시다. Amazon.com이라고 가정 해 봅시다. 신용 카드 유효 기간이 만료 된 상품을 주문한 적이 있습니까? 카드를 업데이트하지 않고 무언가를 구입 한 곳이 있습니다. 주문을 수락하고 UI는 모든 것이 복숭아라는 것을 알려줍니다. 약 15 분 후에 주문에 문제가 있음을 알리는 이메일을 받게되는데 신용 카드가 유효하지 않습니다. 여기서 일어나는 일은 이상적으로 도메인 계층에 정규식 유효성 검사가 있다는 것입니다. 올바른 신용 카드 번호입니까? 그렇다면 주문을 유지하십시오. 그러나 응용 프로그램 작업 계층에는 신용 카드로 결제가 가능한지 외부 서비스를 쿼리하는 추가 유효성 검사가 있습니다. 그렇지 않은 경우 실제로 배송하지 말고 주문을 일시 중지하고 고객을 기다리십시오.

서비스 계층에서 리포지토리에 액세스 할 수 있는 유효성 검사 개체를 만드는 것을 두려워하지 마십시오 . 도메인 계층에서 제외하십시오.


15
감사. 그러나 가능한 한 많은 비즈니스 로직을 엔티티 (및 관련 팩토리 및 사양 등)에 적용하려고 노력해야합니다. 그러나 이들 중 어느 것도 리포지토리를 통해 데이터를 가져올 수 없다면 어떻게 (합리적으로 복잡한) 비즈니스 로직을 작성해야합니까? 예를 들어 : 대화방 사용자는 다른 사람이 이미 사용한 이름으로 이름을 변경할 수 없습니다. ChatUser 엔터티에서 해당 규칙을 작성하고 싶지만 거기에서 리포지토리에 도달 할 수 없으면 그렇게하기가 쉽지 않습니다. 그래서 내가 무엇을해야하니?
codeulike

내 답변이 댓글 상자에서 허용하는 것보다 큽니다. 편집을 참조하십시오.
kertosis

6
귀하는 자신을 해로부터 보호하는 방법을 알아야합니다. 여기에는 유효하지 않은 상태가되지 않도록하는 것이 포함됩니다. 대화방 사용자와 함께 설명하는 것은 엔티티가 자체적으로 유효하게 유지해야하는 로직에 상주하는 비즈니스 로직입니다. 원하는 것은 실제로 UserUser 엔티티가 아닌 Chatroom 서비스에 속하는 것과 같은 비즈니스 논리입니다.
Alec

9
감사합니다. 그것은 그것을 표현하는 명확한 방법입니다. 그러나 나에게는 '모든 비즈니스 로직이 도메인 계층에 가야한다'는 에반스의 도메인 중심 황금률은 '엔터티에 액세스해서는 안된다'라는 규칙과 충돌하는 것으로 보인다. 그 이유를 이해하면 그와 함께 살 수는 있지만 엔티티가 리포지토리에 액세스하지 않아야하는 이유에 대한 온라인 설명을 찾을 수 없습니다. 에반스는 그것을 명시 적으로 언급하지 않는 것 같습니다. 어디에서 왔습니까? 좋은 문헌을 가리키는 답을 게시 할 수 있다면 50pt 현상금을 스스로 챙길 수있을 것입니다 :)
codeulike

4
"그의 작은 이해가되지 않습니다"이것은 팀이 큰 실수입니다 ... 그것은 내가 할 수있는 그런 작은 프로젝트입니다 ... 그런 생각을 중지합니다. 우리가 작업하는 많은 소규모 프로젝트는 비즈니스 요구 사항으로 인해 결국 커집니다. 작거나 큰 일을하는 경우 올바르게하십시오.
MeTitus

35

처음에, 나는 나의 실체 중 일부가 저장소 (즉, ORM이없는 게으른 로딩)에 접근 할 수 있도록 설득했다. 나중에 나는해서는 안되며 다른 방법을 찾을 수 있다는 결론에 도달했습니다.

  1. 요청에 대한 의도와 도메인에서 원하는 것을 알아야하므로 집계 동작을 구성하거나 호출하기 전에 리포지토리 호출을 만들 수 있습니다. 또한 일관성없는 인 메모리 상태 문제와 지연로드가 필요하지 않도록합니다 (이 기사 참조 ). 냄새는 데이터 액세스에 대해 걱정하지 않고 더 이상 엔티티의 메모리 인스턴스를 만들 수 없다는 것입니다.
  2. CQS (Command Query Separation)는 엔티티의 항목에 대한 저장소를 호출하려는 필요성을 줄이는 데 도움이됩니다.
  3. 사양 을 사용하여 도메인 로직 요구를 캡슐화하고 통신하고이를 저장소에 전달할 수 있습니다 (서비스는 이러한 것들을 조정할 수 있습니다). 사양은 해당 불변을 유지하는 책임이있는 실체로부터 나올 수 있습니다. 저장소는 사양의 일부를 자체 쿼리 구현으로 해석하고 사양의 규칙을 쿼리 결과에 적용합니다. 이것은 도메인 계층에서 도메인 로직을 유지하는 것을 목표로합니다. 또한 유비쿼터스 언어와 의사 소통을 더 잘 제공합니다. "지연된 주문 스펙"과 "ts_at가 sysdate 30 분 전인 tbl_order의 필터 주문"이라고 말하는 것을 상상해보십시오 (이 답변 참조 ).
  4. 단일 책임 원칙을 위반하기 때문에 엔터티의 동작에 대한 추론이 더 어려워집니다. 스토리지 / 지속성 문제를 해결해야 할 경우 갈 곳과 갈 곳을 알고 있습니다.
  5. (저장소 및 도메인 서비스를 통해) 엔티티에 글로벌 상태에 대한 양방향 액세스를 제공 할 위험이 없습니다. 또한 거래 경계를 어 기고 싶지 않습니다.

도메인 기반 디자인 구현이라는 빨간 책의 Vernon Vaughn은 내가 알고있는 두 곳에서이 문제를 언급합니다. 서비스의 7 장에서는 도메인 서비스와 사양을 사용하여 집계가 저장소를 사용하고 다른 집계가 사용자 인증 여부를 결정해야하는 필요성을 해결했습니다. 그는 다음과 같이 인용했다.

경험상 우리는 가능한 경우 집계 내부에서 저장소 (12)를 사용하지 않도록 노력해야합니다.

본 버논 (2013-02-06). 도메인 기반 디자인 구현 (Kindle Location 6089). 피어슨 교육. 킨들 에디션.

그리고 10 장 집합에서 "모델 탐색"이라는 제목섹션 (다른 집합 루트를 참조하기 위해 전역 고유 ID 사용을 권장 한 직후)은 다음과 같습니다.

ID로 참조한다고해서 모델을 탐색 할 수있는 것은 아닙니다. 일부는 검색을 위해 집계 내부에서 리포지토리 (12)를 사용합니다. 이 기술을 연결이 끊어진 도메인 모델이라고하며 실제로 지연로드 형식입니다. 그러나 다른 권장 방법이 있습니다. 리포지토리 또는 도메인 서비스 (7)를 사용하여 집계 동작을 호출하기 전에 종속 개체를 찾아보십시오. 클라이언트 응용 프로그램 서비스가이를 제어 한 다음 집계로 전달할 수 있습니다.

그는 코드에서 이것의 예를 보여줍니다.

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

또한 도메인 서비스를 집계 명령 방법에서 double-dispatch 와 함께 사용하는 방법에 대한 또 다른 솔루션을 언급 합니다. (저는 그의 책을 읽는 것이 얼마나 유익한 지 충분히 추천 할 수는 없습니다. 인터넷을 통해 끝없이 뒤섞이는 것에 지친 후, 충분한 돈을 벌고 책을 읽으십시오.)

그런 다음 항상 은혜로운 Marco Pivetta @Ocramius토론 하여 도메인에서 사양을 가져 와서 사용하는 데 약간의 코드를 보여주었습니다.

1) 이것은 권장되지 않습니다 :

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) 도메인 서비스에서 이것은 좋습니다 :

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}

1
질문 : 우리는 항상 유효하지 않거나 일관성이없는 상태에서 객체를 만들지 않도록 배웁니다. 리포지토리에서 사용자를로드 한 다음 getFriends()다른 작업을 수행하기 전에 호출하면 비어 있거나 지연로드됩니다. 비어있는 경우이 개체는 잘못된 상태입니다. 이것에 대한 생각?
Jimbo

리포지토리는 도메인을 호출하여 인스턴스를 새로 만듭니다. 도메인을 거치지 않고는 User 인스턴스를 얻지 못합니다. 이 답변이 해결하는 문제는 그 반대입니다. 도메인이 리포지토리를 참조하는 경우 피해야합니다.
prograhammer

28

아주 좋은 질문입니다. 이에 대한 토론을 기대하겠습니다. 그러나 나는 그것이 여러 DDD 서적 과 Jimmy nilssons와 Eric Evans에 언급되어 있다고 생각합니다 . reposistory 패턴을 사용하는 방법을 예제를 통해 볼 수 있다고 생각합니다.

그러나 토론 할 수 있습니다. 나는 매우 유효한 생각이 다른 엔티티를 유지하는 방법에 대해 엔티티가 알아야하는 이유라고 생각합니다. DDD에서 중요한 것은 각 엔터티가 자체 "지식 영역"을 관리 할 책임이 있으며 다른 엔터티를 읽거나 쓰는 방법에 대해 아는 것이 없다는 것입니다. 엔티티 B를 읽기 위해 엔티티 A에 저장소 인터페이스를 추가 할 수도 있습니다. 그러나 위험은 B를 유지하는 방법에 대한 지식을 노출시키는 것입니다. 엔티티 A도 B를 db로 지속하기 전에 B에 대한 유효성 검증을 수행합니까?

보다시피 엔터티 A는 엔터티 B의 수명주기에 더 많이 관여 할 수 있으며 모델에 더 복잡해질 수 있습니다.

단위 테스팅이 더 복잡 할 것입니다 (예없이).

그러나 엔터티를 통해 리포지토리를 사용하려는 경향이있는 시나리오가 항상있을 것이라고 확신합니다. 유효한 판단을하려면 각 시나리오를 살펴 봐야합니다. 장점과 단점. 그러나 필자의 의견으로는 저장소 엔터티 솔루션은 많은 단점으로 시작합니다. 그것은 단점을 균형 잡는 전문가와 매우 특별한 시나리오 여야합니다 ....


1
좋은 지적. 구식 도메인 모델은 B 엔티티가 지속되기 전에 자체 검증을 책임질 것입니다. Evans가 저장소를 사용하지 않는 엔티티를 언급 했습니까? 나는 책의 반쯤
갔고

글쎄 나는 몇 년 전에 책을 읽었고 (잘 3 ...) 내 기억이 실패합니다. 그가 정확하게 문구를 말하면 기억이 나지 않지만 예제를 통해 이것을 설명했다고 생각합니다. dddsamplenet.codeplex.com 에서 그의화물 사례에 대한 커뮤니티 해석을 찾을 수도 있습니다 ( 자서의 책에서) . 코드 프로젝트를 다운로드하십시오 (바닐라 프로젝트-이 책의 예 참조). 리포지토리는 응용 프로그램 계층에서만 도메인 엔터티에 액세스하는 데 사용됩니다.
Magnus Backeus

1
서적 p2p.wrox.com/ 에서 DDD SmartCA 예제를 다운로드하면 서비스에 리포지토리가 사용되지만 (여기서는 이상하지 않음) 서비스는 엔터티 내부에서 사용되는 또 다른 방법 (RIA Windows 클라이언트 임)을 볼 수 있습니다. 이것은 내가하지 않을 것이지만 나는 웹 응용 프로그램 사람입니다. 오프라인으로 작업 할 수 있어야하는 SmartCA 앱 시나리오를 고려하면 ddd 디자인이 다르게 보일 수 있습니다.
Magnus Backeus

SmartCA 예제는 흥미롭게 들립니다. 어느 장에 있습니까? (코드 다운로드는 장별로 정리되어 있음)
codeulike

1
@ codeulike 나는 현재 ddd 개념을 사용하여 프레임 워크를 설계하고 구현하고 있습니다. 때때로 유효성 검사를 수행하면 데이터베이스에 액세스하여 쿼리해야합니다 (예 : 다중 열 고유 인덱스 검사 쿼리). 이와 관련하여 리포지토리 계층에 쿼리를 작성해야한다는 사실 도메인 엔터티에 대한 참조가 있어야합니다. 도메인 모델 계층에서 유효성 검사를 완전히 수행하기 위해 도메인 모델 계층의 저장소 인터페이스. 따라서 도메인 엔터티가 리포지토리에 액세스 할 수있게되는 것이 마지막입니까?
Karamafrooz

13

왜 데이터 액세스를 분리해야합니까?

이 책에서 Model Driven Design 장의 처음 두 페이지는 도메인 모델 구현에서 기술적 구현 세부 사항을 추상화하려는 이유에 대한 정당성을 제공한다고 생각합니다.

  • 도메인 모델과 코드를 밀접하게 연결하려고합니다.
  • 기술적 문제를 분리하면 모델이 구현에 실용적임을 증명할 수 있습니다
  • 유비쿼터스 언어가 시스템 설계에 스며 들기를 원합니다.

이것은 시스템의 실제 구현과 분리되는 별도의 "분석 모델"을 피하기위한 것입니다.

이 책에서 내가 이해 한 바에 따르면이 "분석 모델"은 소프트웨어 구현을 고려하지 않고도 설계 될 수 있다고합니다. 개발자가 비즈니스 측면에서 이해 한 모델을 구현하려고 시도하면 필요로 인해 자체 추상화를 형성하여 의사 소통과 이해에 벽을 가져옵니다.

다른 한편으로, 도메인 모델에 너무 많은 기술적 인 관심사를 도입 한 개발자는이 차이도 발생할 수 있습니다.

따라서 지속성과 같은 우려 사항을 분리하면 분석 모델이 발산되는 이러한 설계로부터 보호하는 데 도움이 될 수 있습니다. 모델에 퍼시스턴스 (persistence)와 같은 것들을 도입 할 필요가 있다고 느낀다면 그것은 붉은 깃발입니다. 모델이 구현에 실용적이지 않을 수 있습니다.

인용 :

"단일 모델은 설계가 이제 신중하게 고려 된 모델의 직접적인 결과물이기 때문에 오류 발생 가능성을 줄입니다. 설계 및 코드 자체는 모델의 통신 성을 갖습니다."

내가 이것을 해석하는 방식으로, 데이터베이스 액세스와 같은 것들을 다루는 더 많은 코드 줄로 끝내면 그 의사 소통을 잃게됩니다.

데이터베이스에 액세스해야 할 필요성이 고유성을 확인하는 것과 같은 것이라면 다음을 살펴보십시오.

Udi Dahan : DDD를 적용 할 때 팀이 저지르는 가장 큰 실수

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

"모든 규칙이 동일하게 생성되지 않았습니다"에서

도메인 모델 패턴 사용

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

동일한 주제를 다루는 "도메인 모델을 사용하지 않는 시나리오"에서

데이터 액세스를 분리하는 방법

인터페이스를 통한 데이터 로딩

"데이터 액세스 계층"은 필요한 데이터를 검색하기 위해 호출하는 인터페이스를 통해 추상화되었습니다.

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

장점 :이 인터페이스는 "데이터 액세스"배관 코드를 분리하여 테스트를 작성할 수 있습니다. 데이터 액세스는 사례별로 처리 할 수있어 일반적인 전략보다 더 나은 성능을 제공합니다.

단점 : 호출 코드는로드 된 것과로드되지 않은 것을 가정해야합니다.

GetOrderLines가 성능상의 이유로 null ProductInfo 속성을 가진 OrderLine 객체를 반환한다고 가정 해보십시오. 개발자는 인터페이스 뒤의 코드에 대해 잘 알고 있어야합니다.

실제 시스템 에서이 방법을 시도했습니다. 결국 성능 문제를 해결하기 위해로드되는 내용의 범위를 변경하게됩니다. 인터페이스 뒤에 숨어 데이터로드 코드를 살펴보고로드되거나로드되지 않는 것을 확인합니다.

이제 관심사를 분리하면 개발자는 코드의 한 측면에 최대한 집중할 수 있습니다. 인터페이스 기술은이 데이터가로드되는 방법을 제거하지만로드되는 시점과로드 시점에 얼마나 많은 데이터가로드되는지를 제거합니다.

결론 : 상당히 낮은 분리!

게으른 로딩

요청시 데이터가로드됩니다. 데이터를로드하기위한 호출은 객체 그래프 자체에 숨겨져 있으며, 속성에 액세스하면 결과를 반환하기 전에 SQL 쿼리가 실행될 수 있습니다.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

장점 : 데이터 액세스의 'WHEN, WHERE 및 HOW'는 도메인 논리에 중점을 둔 개발자에게 숨겨져 있습니다. 데이터로드를 처리하는 집계에는 코드가 없습니다. 로드되는 데이터의 양은 코드에 필요한 정확한 양일 수 있습니다.

단점 : 성능 문제가 발생했을 때 일반적인 "한 크기에 맞는"솔루션이 있으면 해결하기가 어렵습니다. 지연 로딩은 전체적으로 성능을 저하시킬 수 있으며 지연 로딩을 구현하는 것은 까다로울 수 있습니다.

역할 인터페이스 / 열쇠 가져 오기

각 사용 사례는 집계 클래스에 의해 구현 된 역할 인터페이스 를 통해 명시 적으로 작성 되므로 사용 사례별로 데이터로드 전략을 처리 할 수 ​​있습니다.

페칭 전략은 다음과 같습니다.

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

그런 다음 집계는 다음과 같습니다.

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategy는 집계를 빌드하는 데 사용되며 집계는 작업을 수행합니다.

장점 : 사용 사례별로 사용자 지정 코드를 허용하여 최적의 성능을 제공합니다. 인터페이스 분리 원리 와 일치합니다 . 복잡한 코드 요구 사항이 없습니다. 집계 단위 테스트는로드 전략을 모방하지 않아도됩니다. 일반 로딩 전략은 대부분의 경우에 사용될 수 있으며 (예 : "모두로드"전략) 필요한 경우 특수 로딩 전략을 구현할 수 있습니다.

단점 : 개발자는 도메인 코드를 변경 한 후에도 페칭 전략을 조정 / 검토해야합니다.

페칭 전략 접근 방식을 사용하면 비즈니스 규칙 변경을 위해 사용자 정의 페칭 코드를 변경하는 것을 여전히 알 수 있습니다. 문제를 완벽하게 분리하지는 않지만 유지 관리가 쉬우 며 첫 번째 옵션보다 낫습니다. 페치 전략은 HOW, WHEN 및 WHERE 데이터가로드되는 것을 캡슐화합니다. 하나의 크기가 모든 게으른 로딩 방식에 맞는 것처럼 유연성을 잃지 않으면 서 더 나은 우려 분리 기능을 제공합니다.


감사합니다. 링크를 확인하겠습니다. 그러나 당신의 대답에서 당신은 '문제에 대한 접근이 전혀 없음'과 '문제의 분리'를 혼동하고 있습니까? 확실히 대부분의 사람들은 지속성 계층이 엔티티가있는 계층과 분리되어 있어야한다는 데 동의 할 것입니다. 상호 작용'.
codeulike

인터페이스를 통해 데이터를로드하든 아니든 비즈니스 규칙을 구현하는 동안 데이터를로드하는 데 여전히 관심이 있습니다. 나는 여전히 많은 사람들이이 분리 된 관심을 분리라고 부르는데, 아마도 단일 책임 원칙이 더 나은 용어 일 것입니다.
ttg

1
마지막 주석을 구문 분석하는 방법을 잘 모르지만 비즈니스 규칙을 처리하는 동안 데이터를로드하지 말 것을 제안한다고 생각하십니까? 나는 그것이 규칙을 '순결하게'만들 것이라고 본다. 그러나 많은 유형의 비즈니스 규칙은 다른 데이터를 참조해야합니다. 별도의 개체로 미리로드해야한다고 제안합니까?
codeulike

@ codeulike : 답변을 업데이트했습니다. 비즈니스 규칙 중에 데이터를로드 할 수는 있지만 도메인 모델에 데이터 액세스 코드를 추가 할 필요는 없습니다 (예 : 지연로드). 내가 디자인 한 도메인 모델에서 데이터는 일반적으로 말한 것처럼 미리로드됩니다. 비즈니스 규칙을 실행하면 일반적으로 과도한 양의 데이터가 필요하지 않습니다.
ttg


12

정말 훌륭한 질문입니다. 나는 같은 발견 경로에 있으며 인터넷을 통한 대부분의 답변은 솔루션을 가져 오는 것만 큼 많은 문제를 일으키는 것으로 보입니다.

그래서 (지금부터 1 년 동안 동의하지 않는 것을 쓸 위험이 있음) 여기까지의 발견이 있습니다.

우선, 우리는 풍부한 도메인 모델을 좋아합니다. 이 모델 은 높은 검색 가능성 (집계로 수행 할 수있는 작업)과 가독성 (표현식 메소드 호출)을 제공합니다.

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

우리는 다음과 같은 이유로 엔티티의 생성자에 서비스를 주입하지 않고이를 달성하려고합니다.

  • (새로운 서비스를 사용하는) 새로운 행동을 도입하면 생성자가 변경 될 수 있습니다. 즉, 변경은 엔티티를 인스턴스화하는 모든 행에 영향을 미칩니다 !
  • 이러한 서비스는 모델의 일부가 아니지만 생성자 주입은 해당 서비스를 제안합니다.
  • 서비스 (인터페이스조차도)는 종종 도메인의 일부가 아닌 구현 세부 사항입니다. 도메인 모델은 바깥 쪽을 향한 종속성을 갖습니다 .
  • 이러한 의존성이 없으면 엔티티가 존재할 수없는 이유 가 혼란 스러울 수 있습니다 . (신용 메모 서비스라고합니까? 나는 신용 메모로는 아무 것도하지 않을 것입니다 ...)
  • 인스턴스화를 어렵게하여 테스트 하기가 어렵습니다 .
  • 이 엔티티를 포함하는 다른 엔티티는 동일한 종속성을 갖기 때문에 문제가 쉽게 퍼집니다. 이는 매우 부 자연스러운 종속성 처럼 보일 수 있습니다 .

그러면 어떻게 할 수 있습니까? 지금까지의 결론은 메소드 종속성이중 디스패치 가 적절한 솔루션을 제공한다는 것입니다.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()이제 신용 메모 작성을 담당하는 서비스가 필요합니다. 엔티티 에서 검색 가능성유지 하면서 이중 디스패치를 사용 하여 작업 을 담당 서비스로 완전히 오프로드합니다 .Invoice

SetStatus()이제 로거에 대한 간단한 종속성 이 있으며 이는 분명히 작업의 일부 수행 합니다 .

후자의 경우 클라이언트 코드에서 작업을 더 쉽게하기 위해 대신로 로그인 할 수 있습니다 IInvoiceService. 결국, 인보이스 로깅은 인보이스에 본질적인 것처럼 보입니다. 이러한 단일 기능 IInvoiceService은 다양한 작업을위한 모든 종류의 미니 서비스가 필요하지 않습니다. 단점은이 서비스가 정확하게하는 것이 무엇인지 모호하게한다는 것입니다 수행 . 이중 디스패치처럼 보일 수도 있지만 대부분의 작업은 실제로 SetStatus()자체적으로 수행 됩니다.

의도를 밝히기 위해 매개 변수 이름을 'logger'로 지정할 수 있습니다. 그래도 약간 약한 것 같습니다.

대신 IInvoiceLogger(코드 샘플에서 이미했던 것처럼) 을 요청 IInvoiceService하고 해당 인터페이스를 구현하기로 결정했습니다. 클라이언트 코드는 그 와 같이 매우 특정한 송장 고유의 '미니 서비스'를 요청 IInvoiceService하는 모든 Invoice메소드에 단일 코드를 사용할 수 있으며, 메소드 서명은 여전히 ​​원하는 것을 명확하게 보여줍니다.

리포지토리를 정확하게 처리하지 않은 것으로 나타났습니다. 로거는 저장소이거나 저장소를 사용하지만보다 명확한 예를 제공하겠습니다. 저장소가 한두 가지 방법으로 필요한 경우 동일한 접근 방식을 사용할 수 있습니다.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

실제로, 이것은 항상 문제가되는 게으른 부하에 대한 대안을 제공 합니다 .

업데이트 : 역사적인 목적으로 아래 텍스트를 남겼지 만 게으른 하중을 100 % 제거하는 것이 좋습니다.

사실, 재산 기반의 게으른로드, 난 않는 현재 생성자 주입을 사용하지만 지속성-무식한 방법이다.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

한편으로 Invoice데이터베이스에서 를로드하는 저장소 는 해당 신용 메모를로드하고 해당 함수를에 삽입하는 함수에 자유롭게 액세스 할 수 있습니다 Invoice.

반면에 실제 코드를 작성하는 코드 Invoice는 빈 목록을 리턴하는 함수 만 전달합니다.

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(관습 ILazy<out T>은 우리에게 추한 캐스트를 제거 할 수는 IEnumerable있지만 토론을 복잡하게 만듭니다.)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

귀하의 의견, 선호 사항 및 개선 사항을 기꺼이 듣고 싶습니다!


3

나에게 이것은 DDD에만 국한되지 않고 일반적인 OOD 관련 관행 인 것처럼 보입니다.

내가 생각할 수있는 이유는 다음과 같습니다.

  • 우려 분리 (사용 시나리오에 따라 동일한 엔티티가 유지되는 여러 전략이있을 수 있으므로 엔티티는 유지되는 방식과 분리되어야 함)
  • 논리적으로 저장소는 운영 수준보다 낮은 수준에서 엔티티를 볼 수 있습니다. 하위 레벨 구성 요소는 상위 레벨 구성 요소에 대한 지식이 없어야합니다. 따라서 항목에는 저장소에 대한 지식이 없어야합니다.

2

Vernon Vaughn은 해결책을 제시합니다.

저장소 또는 도메인 서비스를 사용하여 집계 동작을 호출하기 전에 종속 오브젝트를 찾아보십시오. 클라이언트 응용 프로그램 서비스가이를 제어 할 수 있습니다.


그러나 엔티티가 아닙니다.
ssmith

Vernon Vaughn IDDD 출처 : public class Calendar 확장 EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz

그의 논문 @Teimuraz 확인
알리레자 마니 칼 릴리

1

이 모든 별도의 레이어 버즈가 나타나기 전에 객체 지향 프로그래밍을 코딩하는 법을 배웠으며 첫 번째 객체 / 클래스 DID가 데이터베이스에 직접 매핑됩니다.

결국 다른 데이터베이스 서버로 마이그레이션해야했기 때문에 중간 계층을 추가했습니다. 나는 같은 시나리오에 대해 여러 번 보았거나 들었다.

비즈니스 로직에서 데이터 액세스 (일명 "리포지토리")를 분리하는 것이 여러 번 재창조 된 것 중 하나이며 도메인 기반 디자인 책을 채택하여 많은 "노이즈"로 만들었습니다.

현재 많은 개발자와 마찬가지로 3 가지 계층 (GUI, 논리, 데이터 액세스)을 사용합니다. 좋은 기술이기 때문입니다.

데이터를 Repository계층 (일명 Data Access계층) 으로 분리하는 것은 규칙이 아니라 좋은 프로그래밍 기법처럼 보일 수 있습니다.

많은 방법론과 마찬가지로 구현되지 않은 상태에서 시작하여 프로그램을 이해하면 프로그램을 업데이트 할 수 있습니다.

인용구 : Iliad는 호머에 의해 완전히 발명되지 않았고, Carmina Burana는 Carl Orff에 의해 완전히 발명되지 않았으며, 두 경우 모두 다른 일을 한 사람은 모두 togheter입니다.


1
고맙지 만 비즈니스 로직에서 데이터 액세스를 분리하는 것에 대해 묻지는 않습니다. 매우 광범위한 합의가 있습니다. S # arp와 같은 DDD 아키텍처에서 엔터티가 데이터 액세스 계층과 '통화'할 수없는 이유에 대해 묻고 있습니다. 내가 많은 토론을 할 수 없었던 흥미로운 배열입니다.
codeulike

0

이것은 Eric Evans Domain Driven Design 서적에서 나왔습니까 아니면 다른 곳에서 나왔습니까?

오래된 것입니다. 에릭의 책은 방금 조금 더 화를 냈다.

그 뒤에 추론에 대한 좋은 설명이 어디에 있습니까?

이유는 간단합니다. 모호하게 관련된 여러 상황에 직면 할 때 인간의 마음은 약해집니다. 모호함 (남아메리카의 남미는 남미를 의미)으로 이어지고, 모호함은 마음이 "닿을 때마다"정보를 지속적으로 매핑하여 생산성과 오류를 악화시킵니다.

비즈니스 로직은 최대한 명확하게 반영되어야합니다. 외래 키, 정규화, 객체 관계형 매핑은 완전히 다른 도메인에서 온 것입니다. 이러한 것들은 기술적, 컴퓨터 관련입니다.

비유로 : 필기하는 법을 배우는 경우 펜의 위치, 종이에 잉크가 묻은 이유, 종이가 발명 된시기 및 기타 유명한 중국 발명품에 대해 이해해야합니다.

편집 : 명확히하기 위해 : 비즈니스 논리와 별도의 계층으로 데이터 액세스를 분리하는 고전적인 OO 관행에 대해 이야기하고 있지 않습니다 .DDD에서 엔티티가 데이터와 대화하지 않아야하는 특정 배열에 대해 이야기하고 있습니다. 액세스 계층 전혀 없음 (즉, 리포지토리 객체에 대한 참조를 보유하지 않아야 함)

이유는 여전히 위에서 언급 한 것과 같습니다. 한 걸음 더 나아가면됩니다. 엔티티가 완전히 (적어도 가까이)있을 수있는 경우 부분적으로 지속성이 무지해야하는 이유는 무엇입니까? 모델과 관련이없는 도메인 관련 문제가 줄어 듭니다. 다시 해석해야 할 때 더 많은 호흡 공간을 확보 할 수 있습니다.


권리. 그렇다면, 퍼시스턴스 레이어와 대화 할 수없는 경우에도 퍼시스턴스 무지 엔티티는 어떻게 비즈니스 로직을 구현합니까? 임의의 다른 엔티티에서 값을 볼 필요가있을 때 어떻게합니까?
codeulike

엔터티가 임의의 다른 엔터티에서 값을 확인해야하는 경우 일부 디자인 문제가있을 수 있습니다. 어쩌면 수업을 좀 더 응집력있게 분류하는 것이 좋습니다.
cdaq

0

Carolina Lilientahl을 인용하기 위해 "패턴은주기를 방지해야합니다" https://www.youtube.com/watch?v=eJjadzMRQAk , 여기서 클래스 간 순환 종속성을 나타냅니다. 집합체 내에 리포지토리가있는 경우 유일한 이유는 개체 탐색의 편의성에서 주기적 종속성을 생성하려는 유혹이 있습니다. Vernon Vaughn이 추천 한 prograhammer가 위에서 언급 한 패턴은 다른 집계가 루트 인스턴스 대신 ID로 참조되는 경우 (이 패턴의 이름이 있습니까?)는 다른 솔루션으로 안내 할 수있는 대안을 제안합니다.

클래스 사이의 순환 종속성의 예 (고백) :

(Time0) : Sample과 Well의 두 클래스는 서로를 참조합니다 (순환 종속성). Well은 편의상 샘플 (샘플 반복, 때로는 플레이트의 모든 웰을 반복)에서 Well을 다시 참조합니다. 샘플이 배치 된 웰을 다시 참조하지 않는 경우를 상상할 수 없었습니다.

(Time1) : 1 년 후 많은 유스 케이스가 구현되고 .... 이제 샘플이 배치 된 웰을 다시 참조하지 않아야하는 경우가 있습니다. 작업 단계 내에 임시 플레이트가 있습니다. 여기서 우물은 샘플을 말하며, 다른 샘플의 우물을 말합니다. 이로 인해 누군가 새로운 기능을 구현하려고 할 때 이상한 동작이 발생하는 경우가 있습니다. 침투하는 데 시간이 걸립니다.

또한 지연 로딩의 부정적인 측면에 대해 위에서 언급 한이 기사를 통해 도움을 받았습니다 .


-1

이상적인 세계에서 DDD는 엔티티가 데이터 계층을 참조하지 않아야한다고 제안합니다. 그러나 우리는 이상적인 세상에 살고 있지 않습니다. 도메인은 종속성이없는 비즈니스 로직을 위해 다른 도메인 개체를 참조해야 할 수도 있습니다. 엔티티가 읽기 전용으로 저장소 계층을 참조하여 값을 가져 오는 것이 논리적입니다.


아니요, 이는 엔티티에 불필요한 연결을 유발하고 SRP 및 우려 분리를 위반하며 엔티티를 지속성에서 직렬화 해제하는 것을 어렵게 만듭니다.
ssmith
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.