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

后端 未结 11 1339
栀梦
栀梦 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 00:47

    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.

    0 讨论(0)
  • 2020-11-28 00:48

    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.

    0 讨论(0)
  • 2020-11-28 00:49

    There's a bit of a confusion here. Repositories access aggregate roots. Aggregate roots are entities. The reason for this is separation of concerns and good layering. This doesn't make sense on small projects, but if you're on a large team you want to say, "You access a product through the Product Repository. Product is an aggregate root for a collection of entities, including the ProductCatalog object. If you want to update the ProductCatalog you must go through the ProductRepository."

    In this way you have very, very clear separation on the business logic and where things get updated. You don't have some kid who is off by himself and writes this entire program that does all these complicated things to the product catalog and when it comes to integrate it to the upstream project, you're sitting there looking at it and realize it all has to be ditched. It also means when people join the team, add new features, they know where to go and how to structure the program.

    But wait! Repository also refers to the persistence layer, as in the Repository Pattern. In a better world an Eric Evans' Repository and the Repository Pattern would have separate names, because they tend to overlap quite a bit. To get the repository pattern you have contrast with other ways in which data is accessed, with a service bus or an event model system. Usually when you get to this level, the Eric Evans' Repository definition goes by the way side and you start talking about a bounded context. Each bounded context is essentially its own application. You might have a sophisticated approval system for getting things into the product catalog. In your original design the product was the center piece but in this bounded context the product catalog is. You still might access product information and update product via a service bus, but you must realize that a product catalog outside the bounded context might mean something completely different.

    Back to your original question. If you're accessing a repository from within an entity it means the entity is really not a business entity but probably something that should exist in a service layer. This is because entities are business object and should concern themselves with being as much like a DSL (domain specific language) as possible. Only have business information in this layer. If you're troubleshooting a performance issue, you'll know to look elsewhere since only business information should be here. If suddenly, you have application issues here, you're making it very hard to extend and maintain an application, which is really the heart of DDD: making maintainable software.

    Response to Comment 1: Right, good question. So not all validation occurs in the domain layer. Sharp has an attribute "DomainSignature" that does what you want. It is persistence aware, but being an attribute keeps the domain layer clean. It ensures that you don't have a duplicate entity with, in your example the same name.

    But let's talk about more complicated validation rules. Let's say you're Amazon.com. Have you ever ordered something with an expired credit card? I have, where I haven't updated the card and bought something. It accepts the order and the UI informs me that everything is peachy. About 15 minutes later, I'll get an e-mail saying there's a problem with my order, my credit card is invalid. What's happening here is that, ideally, there's some regex validation in the domain layer. Is this a correct credit card number? If yes, persist the order. However, there's additional validation at the application tasks layer, where an external service is queried to see if payment can be made on the credit card. If not, don't actually ship anything, suspend the order and wait for the customer. This should all take place in a service layer.

    Don't be afraid to create validation objects at the service layer that can access repositories. Just keep it out of the domain layer.

    0 讨论(0)
  • 2020-11-28 00:50

    simply Vernon Vaughn gives a solution:

    Use a repository or domain service to look up dependent objects ahead of invoking the aggregate behavior. A client application service may control this.

    0 讨论(0)
  • 2020-11-28 00:53

    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!

    0 讨论(0)
  • 2020-11-28 00:54

    To me this appears to be general good OOD related practice rather than being specific to DDD.

    Reasons that I can think of are:

    • Separation of concerns (Entities should be separated from the way they are persisted. as there could be multiple strategies in which the same entity would be persisted depending on usage scenario)
    • Logically, entities could be seen in a level below the level in which repositories operate. Lower level components should not have knowledge on the higher level components. Therefore entries should not have knowledge on Repositories.
    0 讨论(0)
提交回复
热议问题