当前位置: 动力学知识库 > 问答 > 编程问答 >

What are consequences of using repository inside of aggregate vs inside of domain service

问题描述:

We all heard that injecting repository into aggregate is a bad idea, but almost no one tells why.

I will try to write here all disadvantages of doing this, so we can measure rightness of this statement.

First thing that comes into my head is Single Responsibility Principle.

It's true that by injecting repository into AR we are violating SRP, because retrieving and persisting of aggregate is not responsibility of aggregate itself. But it says only about "aggregate itself", not about other aggregates. So does it apply for retrieving from repository aggregates referenced by id? And what about storing them?

I used to think that aggregate shouldn't even know that there is some sort of persistence in system, because it doesn't have to exist. Aggregates can be created just for one procedure call and then get rid of.

Now when I think of it, it's not right, because aggregate root is an entity, and entity has sense only if it has some unique identity. So why would we need unique identity if not for persisting? Even if it's just a persistence in a memory. Maybe for comparing, but in my opinion it's not a main reason behind the identity.

Ok, let's assume that we retrieve and store OTHER aggregates from inside of our aggregate using injected repositories. What are other consequences beside SRP violation?

For sure there is a problem with having no control over persisting of aggregates and retrieving is some kind of lazy loading, which is bad for the same reason (no control).

Because of no control we can come into situation when we persist the same aggregate few times, where it could be persisted only once, or the same aggregate is loaded one hundred times where it could be loaded once, hence performance is worse. Also there might be problem with stale data.

These reasons practically disqualifies ability to inject repository into aggregate.

Here comes my main question - why can we inject repositories into domain service then?

Not the same reasons applies here? It's just like moving logic out of aggregate into separate function and pretend it to be something different.

To be honest, when I stared to write this SO question, I had no good answer for that. But after hours of investigating this problem and writing of this question I came to solution. Rubber duck debugging.

I'll post this question anyway for others having the same problems. Of course with my answer below.

网友答案:

Here are the places where I'd recommend to fetch aggregates (i.e. call Repository.Get...()), in preference order :

  1. Application Service
  2. Domain Service
  3. Aggregate

We don't want Aggregates to fetch other Aggregates most of the time, because this blurs the lines, giving them orchestration powers which normally belong to the Application layer. You also raise the risk of the Aggregate trespassing its jurisdiction by modifying other Aggregates, which can result in contention and performance problems, not to mention that transactions become more difficult to analyze and the code base to reason about.

Domain Services are IMO a good place to fetch Aggregates when determining which aggregates to modify is domain logic per se. In your game example (which might not be the ideal context for DDD by the way), which units are affected by another unit's attack might be considered domain logic, thus you may not want to place it at the Application Service level. This rarely happens in my experience though.

Finally, Application Services are the default place where I call Repository.Get(...) for uniformity's sake and because this is the natural place to get a hold of the actors of the use case (usually only one Aggregate per transaction) and orchestrate calls to them.

That doesn't mean Aggregates should never be injected Repositories, there are exceptions, but other alternatives are almost always better.

网友答案:

So as I wrote in a question, I've found my answer already in the process of writing that question.

The best way to show this is by example:

When we have a simple (superficially) behavior like unit attacking other unit, we can write something like that.

unit.attack_unit(other_unit)

Problem is that, to attack an unit, we have to calculate damage and to do that we need another aggregates, like weapon and armor, which are referenced by id inside of unit. Since we cannot inject repository inside of aggregate, then we have to move that attack_unit logic into domain service, because we can inject repository there. Now where is the difference between injecting it into domain service, and not into unit aggregate.

Answer is - there is no difference. All consequences I described in question won't bite us. In both cases we will load both units once, attacking unit weapon once and armor of unit being attacked once. Also there won't be stale data, even if we mutate weapon object during process and store it, because that weapon is retrieved and stored in one place.

Problem shows up in different example.

Lets create an use case where unit can attack all other units in game in one process.

Problem lies in how we implement it. If we will use already defined unit.attack_unit and we will call it on all units in game (iterating over them), then weapon that is used to compute damage will be retrieved from unit aggregate, number of times equal to count of units in game! But it could be retrieved only once!

It doesn't matter if unit.attack_unit will be method of unit aggregate, or if it will be domain service unit_attack_unit. It will be still the same, weapon will be loaded too many times. To fix that we simply have to change implementation and with that probably interface too.

Now at least we have an answer to question "does moving logic from aggregate method to domain service (because we want to access repository there) fixes problem?". No, it does not change a thing. Injecting repositories into domain service can be as dangerous as injecting it into aggregate if used wrong.

This answers my SO question, but we still don't have solution to real problem.

What can we do if we have two use cases: one where unit attacks one other unit, and second where unit attacks all other units, without duplicating domain logic.

One way is to put all needed aggregates as parameters to our aggregate method.

unit.attack_unit(unit, weapon, armor)

But what if we will need like five or more aggregates there? It's not a good way. Also application logic will have to know that all these aggregates are needed for an attack, which is knowledge leak. When attack_unit implementation will change we would also might to update interface of that method. What is the purpose of encapsulation then?

So, if we can't access repository to get needed aggregate, how can we smuggle it then?

We can get rid of idea with referencing aggregates by ids, or pass all needed aggregates from application layer (which means knowledge leak).

Or maybe reason of these problems is bad modelling?

Attacking of other unit is indeed an unit responsibility, but is damage calculation its responsibility? Of course not.

Maybe we need another object, like value object MeleeAttack(weapon, armor), yet when we add more properties that can change result of an attack, like enchantments on unit, it gets more complicated. Also I think that we are now creating objects based on performance, not our on domain.

So from domain driven design, we get performance driven design. Is that what we want? I don't think so.

网友答案:

"So why would we need unique identity if not for persisting?" - think of an account scenario, where several John Smiths exist in your system. Imagine John Smith and John Smith Jr (who didn't enter the Jr in signup) both live at the same address. How do you tell them apart? Imagine I'm trying to write a recommendation engine based upon their past purchases . . . .

Identity is a quality of equality in DDD. If you don't have an identity unique from your fields, then you're a ValueObject.

网友答案:

What are consequences of using repository inside of aggregate vs inside of domain service?

There's a reasonably strong argument that you shouldn't do either.

Riddle: when does an aggregate need to see the state of another aggregate?

The responsibility of an aggregate is to control change. Any command that would change the state of the domain model is dispatched to the aggregate root responsible for the integrity of the state in question. By definition, all of the state required to ensure that the command is currently permitted is contained within the aggregate boundary.

So there is never any need to peek at the data outside of the aggregate when making a change to the model.

In which case, you don't ever need to load another aggregate, which makes the "where" question moot.

Two clarifications:

Queries will often combine the state of multiple aggregates, and will often need to follow a reference from one aggregate to another. The principle above is satisfied because queries treat the domain model as read-only. You need the state to answer the query, but you don't need the invariant enforcement because you aren't changing anything.

Another case is when you need state from another aggregate to process a command properly, but small latency in the data is an acceptable risk to the data. In that case, you query the "other" aggregate to get state. If you were to run that query within the domain model itself, the right way to do so would be via a domain service.

In most cases, though, you'll be equally well served to run the query when generating the command (ie, in the client), or when handling the command (in the application, outside the domain). It would be very unusual for a business to consider domain service latency to be acceptable but client latency to be unacceptable.

(Disconnected clients are one case where that can be especially problematic; when the command is generated and then queued for a long period of time before being dispatched to the server).

分享给朋友:
您可能感兴趣的文章:
随机阅读: