The core activity of DDD is building a model, in a team together with developers and domain experts. Good communication between those two groups is essential. Modeling implies defining a common vocabulary. A very specific DDD jargon is not very helpful in that communication and should be avoided. Words like “ubiqitous language”, “bounded context” or “aggregate root” are not inviting non-insiders to share their domain knowledge; I've seen people being scared off by the use of those strange words. In part 1 of this series I investigated the concept of "bounded context". In this second part I propose an alternative for “aggregate root (AR)”.
In the DDD literature the words “aggregate” and “aggregate root” (often abbreviated to AR) are used very frequently. They are core concepts. For developers they sound familiar and are the logical consequence of working with groups of objects, respecting the Law of Demeter and Tell, Don't Ask, while guarding invariants. We have used those terms for decades, from the times we mainly worked with ERDs and ORMs. But for non-technical team members those words are strange jargon.
In DCI (Data-Context-Interaction, see http://fulloo.info/Documents/) the behaviour of objects is specific within the context of a use case: the objects are said to play a “role” in that context. Just like a person has different behaviour playing different “roles” in daily life (for instance as a developer, as a customer, as a mother, as a lover). In a DCI context all the behaviour is gathered that is needed to enact a use case. The roles form invariants in the use case. A context in DCI is comparable to an aggregate in “our” DDD. The behaviour of the roles is comparable to the interface of the aggregate root. Of course there are differences, for in class-oriented programming you cannot easily add behaviour to objects at runtime. But broadly speaking, looking at the similarities instead of at the differences, a DCI context is comparable to a DDD aggregate. An interesting aspect is that the origin of an aggregate is structural (some coherent objects), coming from a data-centric era of development, while a DCI context is mainly based on behavioural coherence, like the concept of invariants is.
One of the good things of DCI contexts is that they correspond one-on-one to use cases. In the 2010 book “Lean Architecture for Agile Software Development”James Coplien and Gertrud Bjørnvig show how well use cases support Agile development. They are good tools for incremental delivery of business value. And they are also familiar concepts for the persons with domain knowledge: we can easily communicate about a succes scenario and alternative scenarios that form a full blown use case. And the focus is immediately where it should be: on the behaviour.
That's why I prefer “use case” above “agregate root”.
It is nothing new that an aggregate root as used in DDD corresponds to a use case. Here are for instance some quotes from Scott Millett's book “Patterns, Principles and Practices of Domain-Driven Design” (2015): “When defining aggregates, you need to identify objects that work together and must be consistent with each other to fulfill business use cases” (page 444), “strive to modify a single aggregate per use case” (page 446), “justify each grouping and ensure that each object is required to define the behaviour of the aggregate instead of just being related to the aggregate”(page 449) and “Focus on modeling aggregates from the perspective of your business use cases” (page 449). However, in “Implementing Domain-Driven Design” (2013) Vaughn Vernon warns us for blindly trusting every use case (page 358-359), but apparently he sees use cases mostly as the product of business analysts, where I see the use cases as emergent from the modeling process, by the joint efforts of the team of developers and domain experts. So be careful with the word “use case” as it has a slightly different meaning for different people. For me a use case is the space between some blue and orange stickies of commands and events in a model storming session; what usually is called an aggregate.
I have a slightly different naming convention for use cases in comparison with aggregate roots, reflecting its behavioural nature: I often use the present participle. It is the place where things happen. The name also mostly fits with the name of commands and events. I get a sequence of command → use case → event: register → registering → registered, pay → paying → payed, etc. (mostly more verbose, including the name of the main object in the name of the command, use case and event, like registerMember etc., but you get the idea). This naming convention shows what is being done in a use case as opposed to the more static name of an object when naming an aggregate root. A use case is more about what the system does (behaviour) than what the system is (structure).
So in my models I treat use cases as first class citizens. Technically seen the use case still is the root of an aggregate, containing all objects involved in the use case. Its methods are not part of one of the domain objects in the aggregate, but of the use case itself. Unfortunately in class-oriented programming we cannot easily add methods to objects at run time, like in “pure” DCI. But we do have all methods belonging to the use case together in one (use case) object. That prevents adding methods to classes for objects that are used in multiple use cases. For instance if a Member would be an object in a registeringMember use case, and that member object would also be used in another use case in the same model (in DDD jargon: within the same Bounded Context) then we can now use that member object in the other use case without the registering methods. The registering methods are only applicable to the member object within the registering use case. I'm aware that this is opposite to the goal of giving domain objects “rich behaviour”, but it has the advantage of having all use case specific behaviour together in one place. It is more in the direction of functional programming (composing behaviour) than object oriented programming (emphasising structure, like relations between objects).
In his 1965 essay “A City is Not a Tree” Christopher Alexander shows the “real world” is too complex to divide it in mutually exclusive concepts. There is a lot of overlap and interference. In DDD we experience that when using multiple models (in DDD jargon: multiple Bounded Contexts) that use the same word for a concept in a different context. For instance a product in the context of a sales model, an inventory model, a fulfillment model etc. Within one model we have a consistent meaning for the name of a domain object, but it can still be used in different use cases, each with different invariants and behaviour. Explicitly naming the use case as such takes away the problem that object behaviour from another use case is part of some aggregate root. I'm still looking for concrete cases, but I guess that in current DDD practice such cases are sometimes solved by adding an extra Bounded Context (although the meaning of the used domain objects is in fact the same and so they should belong to the same model).
I don't have to use technical terms like “aggregate” and “aggregate root” in modeling sessions. We model use cases, consisting of scenarios handling success, failure or edge cases, being invoked by commands and resulting in events. Clear and immediately understandable, also for non-insiders of the DDD jargon.
> email to dddcqrs+unsubscribe@googlegroups.com.
--
Studying for the Turing test
--
You received this message because you are subscribed to the Google Groups "DDD/CQRS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to dddcqrs+unsubscribe@googlegroups.com.
From this perspective, "aggregates" represent code that implements a couple of use-cases, where each use case can be detailed by one or more scenarios.
{
"name": "Test",
"version": 1,
"commands": [
{
"name": "CommandName",
"version": 1,
"assertions": [
{
"type": "isNew",
"parameters": ""
}
],
"events": [
{
"name": "Event1",
"mappings": [
{
"target": "TestString",
"expression": "command.TestString"
},
{
"target": "TestText",
"expression": "command.TestText"
}
]
},
{
"name": "Event2",
"mappings": [
{
"target": "TestBoolean",
"expression": "command.TestBoolean"
},
{
"target": "TestString",
"expression": "command.TestString"
}
]
}
],
"properties": [
{
"name": "TestString",
"type": "string",
"length": "_100"
},
{
"name": "TestText",
"type": "text",
"length": "_100"
},
{
"name": "TestBoolean",
"type": "boolean",
"length": "_100"
}
]
}
],
"events": [
{
"name": "Event1",
"version": 1,
"mappings": [
{
"target": "TestString",
"expression": "event.TestString"
},
{
"target": "TestText",
"expression": "event.TestText"
}
],
"properties": [
{
"name": "TestString",
"type": "string",
"length": "_100"
},
{
"name": "TestText",
"type": "text",
"length": "_100"
}
]
},
{
"name": "Event2",
"version": 1,
"mappings": [
{
"target": "TestBoolean",
"expression": "event.TestBoolean"
},
{
"target": "TestString",
"expression": "event.TestString"
}
],
"properties": [
{
"name": "TestBoolean",
"type": "boolean",
"length": "_100"
},
{
"name": "TestString",
"type": "string",
"length": "_100"
}
]
}
],
"view": {
"version": 1,
"indexes": [
{
"properties": "TestString,TestInteger"
},
{
"properties": "TestBoolean,TestDateTime,TestString"
}
],
"properties": [
{
"name": "TestString",
"type": "string",
"length": "_100"
},
{
"name": "TestBoolean",
"type": "boolean",
"length": "_100"
},
{
"name": "TestText",
"type": "text",
"length": "_100"
},
{
"name": "TestInteger",
"type": "integer",
"length": "_100"
},
{
"name": "TestLong",
"type": "long",
"length": "_100"
},
{
"name": "TestDouble",
"type": "double",
"length": "_100"
},
{
"name": "TestDateTime",
"type": "dateTime",
"length": "_100"
},
{
"name": "TestGuid",
"type": "guid",
"length": "_100"
}
]
}
}
Example domain: planning of crew members for hot air balloons.
A hot air balloon company in the Netherlands has several balloons. Flights are planned by attaching a balloon_id (“call sign”) to a datepart (= date + evening or morning) and a departure place. Every balloon has a fixed list of tasks to which crew members have to be assigned. Tasks are the pilot and the other crew members that drive the support cars. Tasks need skills, for instance a pilot licence of a minimum degree required for the balloon or a driving licence with or without possibility to drive a trailer. Crew members have skills and can online indicate their availability. Flights are planned from a year in advance. Departure place can be changed, for instance due to changed wind directions. A lot of planned flights are canceled due to the weather conditions (rain or too much wind). Passengers of a canceled flight have to make a new reservation for another flight (but that is another model).
The crew members indicate their availability. The planner plans the flights. The system assigns crew members to tasks, based on their availability and the needed skills (and some other preferences). The planner can change an assignment and confirm it. The crew members can then confirm their assignments.
In the use case of changing an assignment the following information is used:
availability of crew members (entity: CrewAvailability)
skills of crew members (Crewmember_Skills)
flight information: datepart and which balloon (Flight)
the needed tasks for a balloon (BalloonTasks)
assignment of crew members to tasks for other flights on that datepart (FlightCrew)
Some invariants:
there can only be one crew member assigned to a task on a flight
a task on a flight can only be assigned to a crew member with at least the needed skills for that task.
in order to be assigned a task on a flight, a crew member has to be available on the datepart of the planned flight.
in order to be assigned a task on a flight, a crew member must not yet been assigned to another planned flight.
The aggregate to guard the invariants involves at least 5 entities (CrewAvailability, Crewmember_Skills, Flight, BalloonTasks and FlightCrew). As Aggregate Root, by which the actions of the use case are handled, we would normally choose the FlightCrew entity, because it is the entity whose state might be affected by the use case. What I did however, is using an object called “Assigning” as root of the aggregate. The methods needed to implement this use case are only used within this use case and the FlightCrew entity can now be used in another aggregate when needed.
The “Assigning” Aggregate Root (or as I call it: the Assigning Use Case) handles the necessary commands with methods like assign(), unassign(), changeAssignment and guards the invariants. Just like it would otherwise be done by the FlightCrew entity as Aggregate Root. In the case of using the FlightCrew entity as Aggregate Root however, it is more difficult to re-use the FlightCrew entity outside the use case, because you then get a situation of multiple aggregates being involved. The FlightCrew entity is then so to say “promoted” to Aggregate Root and stays an AR in the rest of the application. I limit the “assigning” methods to the use case; they are only used as “role” of those entities within the context of that use case and the state of the FlightCrew-entity cannot be changed outside that use case.
The “Assigning” Aggregate Root (or as I call it: the Assigning Use Case) handles the necessary commands with methods like assign(), unassign(), changeAssignment and guards the invariants. Just like it would otherwise be done by the FlightCrew entity as Aggregate Root. In the case of using the FlightCrew entity as Aggregate Root however, it is more difficult to re-use the FlightCrew entity outside the use case, because you then get a situation of multiple aggregates being involved. The FlightCrew entity is then so to say “promoted” to Aggregate Root and stays an AR in the rest of the application. I limit the “assigning” methods to the use case; they are only used as “role” of those entities within the context of that use case and the state of the FlightCrew-entity cannot be changed outside that use case.
To unsubscribe from this group and stop receiving emails from it, send an email to dddcqrs+unsubscribe@googlegroups.com.
Small tip - you are missing a very obvious consistency boundary that eases implementing some of your invariants.
But maybe you meant something else. Am curious. Will first show some actual code (it was written in PHP, hope you don't mind).
// Check the invariants and throw an exception if not OK
$this->taskIsNotYetAssignedToACrewmember($flight, $task);
$this->crewmemberIsAvailableForFlight($flight, $crewmember);
$this->crewmemberIsQualifiedForTask($crewmember, $task);
$this->crewmemberIsNotYetAssignedOnSameDatepart($crewmember, $flight);
class AssigningCrewmembersToFlightTasks extends UseCase
{
/**
* Factory for FlightCrewmembers
*/
private $newFlightCrewmember;
/**
* AssigningCrewmembersToFlightTasks constructor.
*
* @param $newFlightCrewmember
*/
public function __construct
(EventManager $eventManager, NewFlightCrewmember $newFlightCrewmember)
{
parent::__construct($eventManager);
$this->newFlightCrewmember = $newFlightCrewmember;
}
/**
* Scenario: assigning a crewmember to a task that is not yet assigned
* Invoked by AssignCrewmemberToFlightTaskHandler (application layer)
*
* @param Crewmember $crewmember the crewmember to be assigned
* @param Flight $flight the flight it concerns
* @param BalloonTask $task the flight task to be assigned
*
* @uses parent::$eventManager->dispatch(CrewmemberAssignedToFlightTask)
*
* @return void
* @throws RuntimeException when invariants are not met
*/
public function assign(Crewmember $crewmember, Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
$this->taskIsNotYetAssignedToACrewmember($flight, $task);
$this->crewmemberIsAvailableForFlight($flight, $crewmember);
$this->crewmemberIsQualifiedForTask($crewmember, $task);
$this->crewmemberIsNotYetAssignedOnSameDatepart($crewmember, $flight);
// Assign crewmember to flightTask
$flight->addCrewmember(
$this->newFlightCrewmember->create($flight, $task, $crewmember)
);
// Dispatch a CrewmemberAssignedToFlightTask event
$this->eventManager->dispatch(
new CrewmemberAssignedToFlightTask(
$flight->getId(), $task->getId(), $crewmember->getId()
)
);
}
/**
* Scenario: unassigning a crewmember from a task.
* Invoked by UnassignCrewmemberFromFlightTaskHandler.
*
* @param Flight $flight the flight it concerns
* @param BalloonTask $task the flight task to be assigned
*
* @uses parent::$eventManager->dispatch(CrewmemberUnassignedFromFlightTask)
*
* @return void
* @throws RuntimeException when invariants are not met
*/
public function unAssign(Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
// Get the flightcrew-member to whom this task was assigned to
$flightCrewmember = $this->taskIsAssignedToACrewmember($flight, $task);
// remove flight task as being assigned
$flight->removeCrewmember($flightCrewmember);
// Dispatch a CrewmemberAssignedToFlightTask event
$this->eventManager->dispatch(
new CrewmemberUnassignedFromFlightTask(
$flight->getId(), $task->getId(),
$flightCrewmember->getCrewmember()->getId()
)
);
}
/**
* Scenario: assigning a crewmember to an already assigned task.
* First unassign it, then assign,
* taskIsNotYetAssignedToACrewmember is superfluously checked at assignment.
* Invoked by ReassignCrewmemberToFlightTaskHandler
*
* @param Crewmember $crewmember the crewmember to be assigned
* @param Flight $flight the flight it concerns
* @param BalloonTask $task the flight task to be assigned
*
* @uses $this->unAssign($flight, $task)
* @uses $this->assign($crewmember, $flight, $task)
*
* @return void
* @throws RuntimeException when invariants are not met
*/
public function reAssign
(Crewmember $crewmember, Flight $flight, BalloonTask $task)
{
$this->unAssign($flight, $task);
$this->assign($crewmember, $flight, $task);
}
// ========== methods to check invariants ==========
/**
* This flight task should not yet be assigned to a crewmember
*
* @param Flight $flight the flight it concerns
* @param BalloonTask $task the flight task to be assigned
*
* @throw RuntimeException
* @return void
*/
private function taskIsNotYetAssignedToACrewmember
(Flight $flight, BalloonTask $task)
{
// Check the assigned tasks of this flight
$assigned = $flight->findFlightCrewmember($task);
if (! is_null($assigned)) {
throw new RuntimeException(
'This flight task has already been assigned to a crewmember'
);
}
}
/**
* This crewmember should be available for this flight
*
* @param Flight $flight the flight it concerns
* @param Crewmember $crewmember the crewmember to be assigned
*
* @throw RuntimeException
* @return void
*/
private function crewmemberIsAvailableForFlight
(Flight $flight, Crewmember $crewmember)
{
if (!$crewmember->isAvailableOn($flight->getDatepart())) {
throw new RuntimeException(
'This crewmember is not available on the datepart of this flight'
);
}
}
/**
* This crewmember should be qualified for this flight task
*
* @param Crewmember $crewmember the crewmember to be assigned
* @param BalloonTask $task the flight task to be assigned
*
* @throw RuntimeException
* @return void
*/
private function crewmemberIsQualifiedForTask
(Crewmember $crewmember, BalloonTask $task)
{
if (!$crewmember->isQualifiedForTask($task)) {
throw new RuntimeException(
'This crewmember is not qualified for this task'
);
}
}
/**
* This crewmember should not yet be assigned
* to another task on the same datepart
*
* @param Crewmember $crewmember the crewmember to be assigned
* @param Flight $flight the flight it concerns
*
* @throw RuntimeException
* @return void
*/
private function crewmemberIsNotYetAssignedOnSameDatepart
(Crewmember $crewmember, Flight $flight)
{
if (!$crewmember->isNotYetAssignedOn($flight->getDatepart())) {
throw new RuntimeException(
'This crewmember is already assigned on the datepart of this flight'
);
}
}
/**
* This flight task should be assigned to a crewmember
*
* @param Flight $flight the flight it concerns
* @param BalloonTask $task the flight task to be unassigned
*
* @throw RuntimeException
* @return FlightCrewmember $flightCrewmember to which task was assigned
*/
private function taskIsAssignedToACrewmember
(Flight $flight, BalloonTask $task)
{
// Check the assigned tasks of this flight
$assigned = $flight->findFlightCrewmember($task);
if (is_null($assigned)) {
throw new RuntimeException(
'This flight task has not yet been assigned to a crewmember'
);
}
return $assigned;
}
}
throw new <span style="col
Key thing I wanted to show with the example is: here the invariants are guarded in the UseCase, not in for instance the $flight which could have been made an aggregate root. I've done that, because those invariants are only relevant in the context of that specific use case and not in other uses of the domain objects.
In PHP we don't have "friend functions" like in C++, with which we could limit the use of the addCrewmember()-method in the Flight class to the Assigning UseCase. It would be stricter if I would not have an addCrewmember()-method in the Flight class at all. Room for improvement.
With this separation between entities and use cases I have the Clean Architecture picture from https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html in mind:
throw new <span style="col
throw new <span style="col
However, still, I'm not sure why you can't put all this logic in to the aggregate itself.
However, still, I'm not sure why you can't put all this logic in to the aggregate itself.
I'm assuming all commands are processed strictly serially, so that would solve those concurrency problems.
My understanding of Herman's model, is something like this
public function assign(Crewmember $crewmember, Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
$this->taskIsNotYetAssignedToACrewmember($flight, $task);
$this->crewmemberIsAvailableForFlight($flight, $crewmember);
$this->crewmemberIsQualifiedForTask($crewmember, $task);
$this->crewmemberIsNotYetAssignedOnSameDatepart($crewmember, $flight);
// update the flight
$flight = $flight->addCrewmember($
crewmember
);
// Dispatch a CrewmemberAssignedToFlightTask event
$this->eventManager->dispatch(
new CrewmemberAssignedToFlightTask(
$flight->getId(), $task->getId(), $crewmember->getId()
)
);
return $flight
}
Our OOP languages are traditionally more from a structural perspective (being) instead of behavioural (behaving) and temporal (becoming)
Refactoring the AssigningCrewmembersToFlightTasks UseCase
I changed the name of the FlightCrewmember class to CrewmemberAssignment. This object holds the information which crewmember is assigned to which task of a flight. This is the entity that retrieves its data via the ORM from the join-table of the many-to-many relationship between a flight and crewmembers (fullfilling the tasks that the used balloon requires). In the table we currently have records with a flight_id, task_id and crewmember_id. The word “assignment” better describes the information this object represents. A simple name change but it triggered other thoughts.
I wanted to take the responsability for the assignment away from the Flight class. The flight can query what tasks are needed, what tasks are assigned (and consequently what is still open). But the flight cannot assign crewmembers to its tasks anymore. Of course, under the hood you could still just do an add() or removeElement() on the Doctrine Collection that forms the assigned crew at that moment, but there is no public method to do that directly on the flight-object anymore.
As said, I had given the responsibility for assigning to the Assigning use case. But where are the Assignment-objects added to and where is that done? It is not the Assignment-objects themselves, but something that manages the collection of those objects. So I came to the Assignment Repository. A Repository has two legs: one is standing in the domain layer (interface: managing a collection of objects) and the other in the infrastructure layer (managing persistence). It is a mediator between those two layers. In Scott Millet's book, chapter 21, NHibernate is used as example and an interface is used for both retrieving from the database (the find-methods) as for adding and removing objects to and from the collection, persisting those changes from there in the database. But as Eric Evans describes in his chapter about Repositories (page 147-167), the Repository pattern was originally not intended for storing newly created objects, merely for reconstituting objects from the database. A Factory on the other hand handles the beginning of an object's life. In this balloon-project we use Doctrine ORM and a Repository is there only used for retrieving objects from the database; no add() or remove() in that interface.
So I use a CrewmemberAssignmentFactory to create a new CrewmemberAssignment-object (and make it manageable to be persisted when the whole transaction is successfuly ended). No need to explicitly add it to a crew-collection of the flight: when the transaction is committed (= when the Unit of Work is flushed) the database and the collections in various entities are synchronised. My domain layer doesn't have to know about that: it is completely agnostic of any persistence. Clean separation of concerns in the different layers.
Now I still needed to be able to remove a CrewmemberAssignment object. As the Doctrine Repository was no option I had a look at my CrewmemberAssignmentFactory again. That may seem odd at first and maybe it would need another name than Factory, but when looking at it from a bit abstract level removing is a kind of negative adding. One is moving an object into a collection (and persist that in the database) the other is moving it out of the collection (and persist that in the database). You can always transform those two different actions into one (plus an extra boolean parameter to give the direction) and then persist that append-only. I had also used that with the objects that indicate the availability of crewmembers: they indicate if a crewmember is available or unavailable on the given days, dateparts or date-ranges. I will come back on this in my answer to Johanna's question why I choose to include both assign and unassign methods in my Assigning use case. Also see Greg's “Answering a question”-talk from june 2014 https://skillsmatter.com/skillscasts/5437-answering-a-question#video where he uses an example with a kind of “negative membership” instead of removing a member. In short: besides an add()-method I made a remove()-method in the CrewmemberAssignmentFactory to let the infrastructure know the removal of the CrewmemberAssignment has to be persisted in the database too.
Now both creating and deleting persistable objects were with a simple method in that Factory (or whatever name we should give it), I looked at the possibility to also have the invariants guarded there. But that is not a good idea: that business logic belongs to the use case and not to the infrastructural layer. The only responsability of that “Factory” is to create and remove objects in such a way that it can be persisted.
In a relational database we have a join-table with foreign keys from tables that have a many-to-many relationship. Between the flights-table and the crewmember-table I had such a FlightCrewmember-table (also having a column for the task that the crewmember is assigned to). From there the entity holding the collection of crewmembers assigned to a flight was first also called FlightCrewmember. The same with a CrewmemberSkill table for the many-to-many relation between crewmembers and skills. After thinking a bit what is actually meant by this FlightCrewmember-object, I renamed it to CrewmemberAssignment (first named it CrewmemberToFlightTaskAssignment, but that is unnecessary longer). I did the same with the CrewmemberSkill-object and renamed that CrewmemberQualification.
So I have a verb imperatively describing an action: assign or qualify. I use that same verb also in the form of a past participle for events (assigned or qualified). In the form of a present participle for use cases (assigning or qualifying). And now I also use a noun (assignment, qualification) as name for an entity, a thing that is added and removed. Interesting how you can switch from verb to noun and vice-versa in the same context but with a bit different use. It reminds me of switching from RPC to REST, where you add to an assignment-resource instead of remotely calling an assign-procedure and delete from that assignment-resource instead of remotely calling an unassign-procedure. Rich behaviour of objects can always be translated to a simple, uniform CRUD-interface with a rich variety of objects (resources) and vice-versa. One is not necessarily better than the other; it is all about the usefulness in different situations.
The code of the invariants was unchanged. Except for the above mentioned refactoring I did some minor changes. Currently the code of the 3 public methods of the AssigningCrewmembersToFlightTasks use case, without the docblocks, looks like this:
public function assign(Crewmember $crewmember, Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
$this->taskIsNotYetAssignedToACrewmember($flight, $task);
$this->crewmemberIsAvailableForFlight($crewmember, $flight);
$this->crewmemberIsQualifiedForTask($crewmember, $task);
$this->crewmemberIsNotYetAssignedOnSameDatepart($crewmember, $flight);
// Add assignment of crewmember to flightTask
$crewmemberAssignment
= $this->crewmemberAssignmentFactory->add($flight, $task, $crewmember);
// Dispatch a CrewmemberAssignedToFlightTask event
$this->eventManager->dispatch(
new CrewmemberAssignedToFlightTask($crewmemberAssignment)
);
}
public function unAssign(Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
// Get the CrewmemberAssignment to whom this task was assigned to
$crewmemberAssignment = $this->taskIsAssignedToACrewmember($flight, $task);
// remove flight task as being assigned
$this->crewmemberAssignmentFactory->remove($crewmemberAssignment);
// Dispatch a CrewmemberUnassignedFromFlightTask event
$this->eventManager->dispatch(
new CrewmemberUnassignedFromFlightTask($crewmemberAssignment)
);
}
public function reAssign
(Crewmember $crewmember, Flight $flight, BalloonTask $task)
{
// Check the invariants and throw an exception if not OK
$this->crewmemberIsAvailableForFlight($crewmember, $flight);
$this->crewmemberIsQualifiedForTask($crewmember, $task);
$this->crewmemberIsNotYetAssignedOnSameDatepart($crewmember, $flight);
// Get the CrewmemberAssignment to whom this task was assigned to
$crewmemberAssignment = $this->taskIsAssignedToACrewmember($flight, $task);
// --- Remove flight task as being assigned ---
$this->crewmemberAssignmentFactory->remove($crewmemberAssignment);
// Dispatch a CrewmemberUnassignedFromFlightTask event
$this->eventManager->dispatch(
new CrewmemberUnassignedFromFlightTask($crewmemberAssignment)
);
// --- Add assignment of crewmember to flightTask ---
$this->crewmemberAssignmentFactory->add($flight, $task, $crewmember);
// Dispatch a CrewmemberAssignedToFlightTask event
$this->eventManager->dispatch(
new CrewmemberAssignedToFlightTask($crewmemberAssignment)
);
}
May I ask what criteria you use to determine the boundaries between use cases? For example, why did you choose to include both assign and unassign methods in your Assigning use case?
(...)
So it's all about the Bounded Context concept denial.
Wouldn't be you happy with something like partial classes like in C#, so you can make every method of an aggregate in different files? Every file (method) is a "use case", you can't change the state outside of these files ("use cases").
I have feeling that you don't like BC concept because sometimes it leads to some sort of duplication of your model. Sometimes you may have similar User aggregates in different BCs, so you think about it as about same model.
Duplication is the price for isolation and independence.
Bounded Context is yet another tool to follow the SRP. SRP is not about "every class must have the only method". Responsibility of a model is to represent a concept from a domain.
Different use cases doesn't always break this responsibility, it may still represent a concept.
Refactoring the AssigningCrewmembersToFlightTasks UseCase
All those points are interesting and thank you for bringing it up but not the subject of this thread. The domain is just a concrete example so we are not only talking theoretically. It is much much more elaborate than what I show here; it is a concrete, but simplified example. Although I appreciate all ideas about the domain and the modeling of it I want to take care that I won't loose the focus of this thread: that I see validity in giving use cases (behaviour) a more central role in my models than aggregates (structure).
keep up the good work!
i want to he rich so i can hire / throw money at you.
The example in this thread is only about the crew-planning.
All those models share concepts, like a Flight or Balloon, but they all use different aspects of that concept. We might split into more models and refactor to a Shared Kernel. And there is a lot more that will get its own model when it will be developed further (group flights, gift vouchers, the coupling to accounting, friends vouchers for passengers via their network, etc.); plans enough.
On the other hand: this example is from an application we have been and will be working on over a longer period of time with only a very small team (mainly due to the very limited budgets). In compliance with Conway's Law the small team will be reflected in a tendency to avoid splitting models up very much. But that is more from practical than theoretical motives.
> I changed the name of the FlightCrewmember class to CrewmemberAssignment. This object holds the information which crewmember is assigned to which task of a flight.
The notion that there's the entity in the role of Crewmember who hasn't actually been assigned to the flight crew would drive me absolutely bonkers. My instinct is that there really should be ubiquitous language for flight qualified employees that is independent of their current assignments in the duty roster.
Also CrewmemberAssignment sounds backwards. We aren't starting with a flight, and Bob, and trying to find something for Bob to do; we're starting with a flight and a responsibility (pilot!) and trying to ensure that the responsibilities are all being covered by somebody qualified to handle them. So I would expect TaskAssignment/DutyAssignment, rather than CrewAssignment. Perhaps I'm drawing from other domains, where you might have crew pulling multiple duties on a single flight.
The name for this thing, whatever it is, should reflect what's used in the business; if it is a crewmember assignment, then so be it... unless the business is open to the suggestion that a clearer language should be adopted. We are allowed to improve the business as part of this modeling exercise, after all.
My understanding of Herman's model, is something like this
1) The entire model is contained within a single consistency boundary;
any command effectively locks down the state of everything entity in the design until the processing is completed.
1a) From which it follows that the model (all of it) is the aggregate.
2) The design has no explicit aggregate root; at least, not one that I have been able to identify in the design thus far.
AssigningCrewmembersToFlightTasks
use case there is only one entity that is changed: we create or delete a CrewmemberToFlightTaskAssignment object. Nothing under it, so this is the one and only entity in the aggregate and hence the "aggregate root" in this use case. In other use cases other entities are changed. I try to keep the depth of aggregates as shallow as possible (also to respect Demeter's Law) so if it is possible to do the change in one entity then my aggregate is just that one entity (and hence the aggregate root).3) The definition of the business invariant, which in an OO design would normally be implemented within the entity command methods, are instead given first class status as use cases.
4) The entities themselves are data structures. Which is to say, that they are state.
It's not all that hard to envision that the arguments being passed into the use cases could be value types
> No need to explicitly add it to a crew-collection of the flight: when the transaction is committed (= when the Unit of Work is flushed) the database and the collections in various entities are synchronised.
I'm troubled by the fact that the persistence of new entities created by the factory is implicit, rather than explicit. It seems to be "clever", that is to say, a surprise. This stuff is hard enough to get right even when we are boring.