Reconstitute part of an aggregate only in the repo

2019-08-01 09:06发布

问题:

I have a Bounded Context where two Aggregates are central to the business in that particular model. The model is starting to express quite well, but the association between those needs to be bidirectional (I am exploring and making alternative designs, so probably I ended up finding a better approach).

Let's say A and B are the aggregate roots of each aggregate. A and B has a n:m association. Invariants around A need track at least the number of instances of B, and there is high concurrency contention on A (many users modifying instances of A at the same time, very likely the same A) and business says invariant here must be enforced (no eventual consistency is allowed). If an user is trying to "attach" an instance of B to an instance of A (this has a concept in the UL, of course), the rules requiere inspecting the current state of B, including the instances of other As it relates to. This is crucial, because the operation can be denied depending on the analysis and so the aggregate's invariants are preserved. There is low concurrency contention on B (it's very, very unlikely that different users will attempt to modify the same B, and is probably a mistake by the user).

Since there is low contention on B, there is little damage on save both aggregates in the same transaction. However the "attach" operation on A requieres only the current state of B, so it is possible to keep them out of sync in the current transaction, but the next one (that could be started immediately) needs to see the changes of the last one.

I know by reading the Blue Book and the Red Book that a Repository is allowed to return, in some cases, a Value Object instead of an entire aggregate, but what about let the repository hydrate part of the state of B derived from the state of A at the database level?

The current transaction could mutate and save the state of A without modifying B. Then the next transaction retrieves B again, but this time the changes are visible since B is regenerated inspecting the state of A in the repository.

My question is not about the implementation itself, it's about the model: with this approach the model expresses that A relates to many Bs and A has the right operation to modify that collection (the "attach" operation). But even if B relates to many As, the use cases requiere to navigate from B to its As and therefore a method on A allows this navigation, this collection of A's inside B is not managed explicitly by the "attach" operation since now A doesn't have a method to mutate its collection; it's implemented in the infrastructure inside the Repository, through a database lookup and reconstitution. Yes, the code model still expresses the existence of the relationship from B to A, but is unclear where it comes from and how is calculated.

What should I favor? The more explicit code model that keeps the bidirectional association in code, managed through the "attach" operation, even if it involves dealing with maintaining references in sync and mutate two different aggregates in the same transaction; or let the repository reconstitute one of those aggregates, examining the state of the persisted state, but leaving part of the semantics of the association inside the repository implementation?

UPDATE: More context

The domain, simplified, is very similar to this: an educational center where there are classrooms, courses and students. Students are primarily managed in another BC (personal data, etc) but here they can also mutate since they are allowed to change the contract/agreement almost at any time (this is managed in another BC too), and that determines the kind of courses they can be enrolled in. The unique number of that contract is relevant and the Student entity needs to keep track of them. The student start to attends the classes but can change to another course at any time, so all the records about attendance (scores, etc) belongs to the Student entity.

An user meet with an student and the user register the student in courses based on the current contract, but there are many rules for that beyond capacity, most of which requiere to explore the current status of the Student, as previous assignments or attendance. The business is quite restrictive in that and one of the system goals is to enforce those rules without leaving lot of choices users. Different strategies of enrollment can be picked based on the permissions of the user, though. Oh, and there are many users performing the same operation with different students at the same time, and business request that users needs to see the changes the "most real time" as possible.

There are more rules of course, some of them relating to the capacity of the course, but that is a glimpse of the domain. I modeled both, Course (before referenced as A) and Student (before as B) as Aggregate Roots. To fulfill the enrollment use case (the "attach" operation) the Course is retrieved from its Repository and collaborates with a Domain Service. Course needs the ids to its current students to, at least, calculate its capacity, since another user can be modifying the same course at the same time (a concurrency exception is thrown and rules must be validated again); there is no use case to hold a direct reference to a Student from the Course, only by id. An instance of Student is needed to inspect its current contract and the Courses is/have been enrolled, including those of the "same session of enrollment", in order to decide the result of the operation requested.

回答1:

As you pointed out, there needs to be a better model. The bidirectional relationship already hints that this model does not reflect what is happening. I'm not saying bidirectional references are always bad, but they are always a "hint".

Sounds to me you are modeling the "relationship" between these two things and not the behavior. This is further reinforced in that you don't really describe what these things actually do. You can't model something based just on the data and how the data relates to eachother.

So first, please extend the description with the actual business function these objects need to fulfill. That is anything other than store, retrieve, load, list, select or these things. I mean business functions.

For example if B has no real function (it's just data), it can be simply rolled into A. Etc..

Re: More context

Ok, what I can gather from your description, the business case is enrollment of a Stundent in a Course. And the problem is, that there are rules which may or may not require knowledge about both of those things.

I would perhaps go this way: Since we need both of those things, it seems something is missing. I would call that thing Enrollments for now. Here is some Java-pseudo code how that would work:

public interface Enrollments {
    Student find(String name); // Or whatever is needed
}

public interface Student {
    List<Course> findEnrollableCourses();
}

public interface Course {
    void enroll();
}

In this model Course does not model a course independently, it is always in a context of a Student. And the Student is in the context of Enrollments. This means I can enroll a Student in a Course where all the "data" for this enrollment stays in this semantic context (or aggregate root). This means that in the enroll() method I have access to all data "legally". There is no need for circular dependencies between these things, there is a clear hierarchy.