DDD - the rule that Entities can't access Repositories directly

后端 未结 11 1449
栀梦
栀梦 2020-11-28 00:24

In Domain Driven Design, there seems to be lots of agreement that Entities should not access Repositories directly.

Did this come from Eric Evans Domain Driven Desi

11条回答
  •  悲哀的现实
    2020-11-28 01:03

    At first, I was of the persuasion to allow some of my entities access to repositories (ie. lazy loading without an ORM). Later I came to the conclusion that I shouldn't and that I could find alternate ways:

    1. We should know our intentions in a request and what we want from the domain, therefore we can make repository calls before constructing or invoking Aggregate behavior. This also helps avoid the problem of inconsistent in-memory state and the need for lazy loading (see this article). The smell is that you cannot create an in memory instance of your entity anymore without worrying about data access.
    2. CQS (Command Query Separation) can help reduce the need for wanting to call the repository for things in our entities.
    3. We can use a specification to encapsulate and communicate domain logic needs and pass that to the repository instead (a service can orchestrate these things for us). The specification can come from the entity that is in charge of maintaining that invariant. The repository will interpret parts of the specification into it's own query implementation and apply rules from the specification on query results. This aims to keep domain logic in the domain layer. It also serves the Ubiquitous Language and communcation better. Imagine saying "overdue order specification" versus saying "filter order from tbl_order where placed_at is less than 30 minutes before sysdate" (see this answer).
    4. It makes reasoning about the behavior of entities more difficult since the Single-Responsibility Principle is violated. If you need to work out storage/persistence issues you know where to go and where not to go.
    5. It avoids the danger of giving an entity bi-directional access to global state (via the repository and domain services). You also don't want to break your transaction boundary.

    Vernon Vaughn in the red book Implementing Domain-Driven Design refers to this issue in two places that I know of (note: this book is fully endorsed by Evans as you can read in the foreword). In Chapter 7 on Services, he uses a domain service and a specification to work around the need for an aggregate to use a repository and another aggregate to determine if a user is authenticated. He's quoted as saying:

    As a rule of thumb, we should try to avoid the use of Repositories (12) from inside Aggregates, if at all possible.

    Vernon, Vaughn (2013-02-06). Implementing Domain-Driven Design (Kindle Location 6089). Pearson Education. Kindle Edition.

    And in Chapter 10 on Aggregates, in the section titled "Model Navigation" he says (just after he recommends the use of global unique IDs for referencing other aggregate roots):

    Reference by identity doesn’t completely prevent navigation through the model. Some will use a Repository (12) from inside an Aggregate for lookup. This technique is called Disconnected Domain Model, and it’s actually a form of lazy loading. There’s a different recommended approach, however: Use a Repository or Domain Service (7) to look up dependent objects ahead of invoking the Aggregate behavior. A client Application Service may control this, then dispatch to the Aggregate:

    He goes onto show an example of this in code:

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

    He goes on to also mention yet another solution of how a domain service can be used in an Aggregate command method along with double-dispatch. (I can't recommend enough how beneficial it is to read his book. After you have tired from end-lessly rummaging through the internet, fork over the well deserved money and read the book.)

    I then had some discussion with the always gracious Marco Pivetta @Ocramius who showed me a bit of code on pulling out a specification from the domain and using that:

    1) This is not recommended:

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

    2) In a domain service, this is good:

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

提交回复
热议问题