DDD - the rule that Entities can't access Repo

2020-01-25 03:19发布

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 Design book, or did it come from elsewhere?

Where are there some good explanations for the reasoning behind it?

edit: To clarify: I'm not talking about the classic OO practice of separating data access off into a separate layer from the business logic - I'm talking about the specific arrangement whereby in DDD, Entities are not supposed to talk to the data access layer at all (i.e. they are not supposed to hold references to Repository objects)

update: I gave the bounty to BacceSR because his answer seemed closest, but I'm still pretty in the dark about this. If its such an important principle, there should be some good articles about it online somewhere, surely?

update: March 2013, the upvotes on the question imply there's a lot of interest in this, and even though there's been lots of answers, I still think there's room for more if people have ideas about this.

12条回答
看我几分像从前
2楼-- · 2020-01-25 03:34

I learnt to code object oriented programming before all this separate layer buzz appear, and my first objects / classes DID map directly to the database.

Eventually, I added an intermediate layer because I had to migrate to another database server. I have seen / heard about the same scenario several times.

I think separating the data access (a.k.a. "Repository") from your business logic, is one of those things, that have been reinvented several times, altought the Domain Driven Design book, make it a lot of "noise".

I currently use 3 layers (GUI, Logic, Data Access), like many developer does, because its a good technique.

Separating the data, into a Repository layer (a.k.a. Data Access layer), may be seen like a good programming technique, not just a rule, to follow.

Like many methodologies, you may want to start, by NOT implemented, and eventually, update your program, once you understand them.

Quote: The Iliad wasn't totally invented by Homer, Carmina Burana wasn't totally invented by Carl Orff, and in both cases, the person who put others work, all togheter, got the credit ;-)

查看更多
再贱就再见
3楼-- · 2020-01-25 03:34

To cite Carolina Lilientahl, "Patterns should prevent cycles" https://www.youtube.com/watch?v=eJjadzMRQAk, where she refers to cyclic dependencies between classes. In case of repositories inside aggregates, there is a temptation to create cyclic dependencies out of conveniance of object navigation as the only reason. The pattern mentioned above by prograhammer, that was recommended by Vernon Vaughn, where other aggregates are referenced by ids instead of root instances, (is there a name for this pattern?) suggests an alternative that might guide into other solutions.

Example of cyclic dependency between classes (confession):

(Time0): Two classes, Sample and Well, refer to each other (cyclic dependency). Well refers to Sample, and Sample refers back to Well, out of convenience (sometimes looping samples, sometimes looping all wells in a plate). I couldn't imagine cases where Sample would not reference back to the Well where it's placed.

(Time1): A year later, many use cases are implemented .... and there are now cases where Sample should not reference back to the Well it's placed in. There are temporary plates within a working step. Here a well refers to a sample, which in turn refers to a well on another plate. Because of this, weird behaviour sometimes occurs when somebody tries to implement new features. Takes time to penetrate.

I also was helped by this article mentioned above about negative aspects of lazy loading.

查看更多
ら.Afraid
4楼-- · 2020-01-25 03:34

In the ideal world , DDD proposes that Entities should not have reference to data layers. but we do not live in ideal world. Domains may need to refer to other domain objects for business logic with whom they might not have a dependency. It is logical for entities to refer to repository layer for read only purpose, to fetch the values.

查看更多
虎瘦雄心在
5楼-- · 2020-01-25 03:35

I found this blog to have quite good arguments against encapsulating Repositories inside Entities:

http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities

查看更多
祖国的老花朵
6楼-- · 2020-01-25 03:37

What an excellent question. I am on the same path of discovery, and most answers throughout the internet seem to bring as many problems as they bring solutions.

So (at the risk of writing something that I disagree with a year from now) here are my discoveries so far.

First of all, we like a rich domain model, which gives us high discoverability (of what we can do with an aggregate) and readability (expressive method calls).

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

We want to achieve this without injecting any services into an entity's constructor, because:

  • Introduction of a new behavior (that uses a new service) could lead to a constructor change, meaning the change affects every line that instantiates the entity!
  • These services are not part of the model, but constructor-injection would suggest that they were.
  • Often a service (even its interface) is an implementation detail rather than part of the domain. The domain model would have an outward-facing dependency.
  • It can be confusing why the entity cannot exist without these dependencies. (A credit note service, you say? I am not even going to do anything with credit notes...)
  • It would make it hard instantiate, thus hard to test.
  • The problem spreads easily, because other entities containing this one would get the same dependencies - which on them may look like very unnatural dependencies.

How, then, can we do this? My conclusion so far is that method dependencies and double dispatch provide a decent solution.

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() now requires a service that is responsible for creating credit notes. It uses double dispatch, fully offloading the work to the responsible service, while maintaining discoverability from the Invoice entity.

SetStatus() now has a simple dependency on a logger, which obviously will perform part of the work.

For the latter, to make things easier on the client code, we might instead log through an IInvoiceService. After all, invoice logging seems pretty intrinsic to an invoice. Such a single IInvoiceService helps avoid the need for all sorts of mini-services for various operations. The downside is that it becomes obscure what exactly that service will do. It might even start to look like double dispatch, while most of the work is really still done in SetStatus() itself.

We could still name the parameter 'logger', in hopes of revealing our intent. Seems a bit weak, though.

Instead, I would opt to ask for an IInvoiceLogger (as we already do in the code sample) and have IInvoiceService implement that interface. The client code can simply use its single IInvoiceService for all Invoice methods that ask for any such a very particular, invoice-intrinsic 'mini-service', while the method signatures still make abundantly clear what they are asking for.

I notice that I have not addressed repositories exlicitly. Well, the logger is or uses a repository, but let me also provide a more explicit example. We can use the same approach, if the repository is needed in just a method or two.

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

In fact, this provides an alternative to the ever-troublesome lazy loads.

Update: I have left the text below for historical purposes, but I suggest steering clear of lazy loads 100%.

For true, property-based lazy loads, I do currently use constructor injection, but in a persistence-ignorant way.

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

On the one hand, a repository that loads an Invoice from the database can have free access to a function that will load the corresponding credit notes, and inject that function into the Invoice.

On the other hand, code that creates an actual new Invoice will merely pass a function that returns an empty list:

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

(A custom ILazy<out T> could rid us of the ugly cast to IEnumerable, but that would complicate the discussion.)

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

I'd be happy to hear your opinions, preferences, and improvements!

查看更多
乱世女痞
7楼-- · 2020-01-25 03:38

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); 
}
查看更多
登录 后发表回答