Preventing external object to call methods on entity inside an aggregate
According to DDD principles, external objects should only call methods on an aggregate root, not on other entities in the aggregate, right ?
In case of nested entities, for example: SeatingPlan -> Sections -> Rows -> Seats
SeatingPlan is the aggregate root, while sections, rows and seats are entities that are meaningless outside its parent entity.
Lets say I want to add seats in the seating plan.
I would create SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to call SeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right ?
But still, the AddSeat
method of SeatingPlan must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to call Sections[x].Rows[y].AddSeat(seatNo)
.
Now my question is how can I prevent external objects from calling Row.AddSeat
method, while allowing the aggregate root to call it ?
internal visibility is too large, even namespace visibility (assuming it would even exists in c#) would be too large. I need an aggregate visibility.
I thought about nesting the class Row in the SeatingPlan
class, and making the Row.AddSeat
method private. But is it a good practice ? Because the class would have to be public and I remember having read something about it saying that we should avoid public nested classes.
c# domain-driven-design aggregate
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root, not on other entities in the aggregate, right ?
In case of nested entities, for example: SeatingPlan -> Sections -> Rows -> Seats
SeatingPlan is the aggregate root, while sections, rows and seats are entities that are meaningless outside its parent entity.
Lets say I want to add seats in the seating plan.
I would create SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to call SeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right ?
But still, the AddSeat
method of SeatingPlan must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to call Sections[x].Rows[y].AddSeat(seatNo)
.
Now my question is how can I prevent external objects from calling Row.AddSeat
method, while allowing the aggregate root to call it ?
internal visibility is too large, even namespace visibility (assuming it would even exists in c#) would be too large. I need an aggregate visibility.
I thought about nesting the class Row in the SeatingPlan
class, and making the Row.AddSeat
method private. But is it a good practice ? Because the class would have to be public and I remember having read something about it saying that we should avoid public nested classes.
c# domain-driven-design aggregate
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
1
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root, not on other entities in the aggregate, right ?
In case of nested entities, for example: SeatingPlan -> Sections -> Rows -> Seats
SeatingPlan is the aggregate root, while sections, rows and seats are entities that are meaningless outside its parent entity.
Lets say I want to add seats in the seating plan.
I would create SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to call SeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right ?
But still, the AddSeat
method of SeatingPlan must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to call Sections[x].Rows[y].AddSeat(seatNo)
.
Now my question is how can I prevent external objects from calling Row.AddSeat
method, while allowing the aggregate root to call it ?
internal visibility is too large, even namespace visibility (assuming it would even exists in c#) would be too large. I need an aggregate visibility.
I thought about nesting the class Row in the SeatingPlan
class, and making the Row.AddSeat
method private. But is it a good practice ? Because the class would have to be public and I remember having read something about it saying that we should avoid public nested classes.
c# domain-driven-design aggregate
According to DDD principles, external objects should only call methods on an aggregate root, not on other entities in the aggregate, right ?
In case of nested entities, for example: SeatingPlan -> Sections -> Rows -> Seats
SeatingPlan is the aggregate root, while sections, rows and seats are entities that are meaningless outside its parent entity.
Lets say I want to add seats in the seating plan.
I would create SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to call SeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right ?
But still, the AddSeat
method of SeatingPlan must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to call Sections[x].Rows[y].AddSeat(seatNo)
.
Now my question is how can I prevent external objects from calling Row.AddSeat
method, while allowing the aggregate root to call it ?
internal visibility is too large, even namespace visibility (assuming it would even exists in c#) would be too large. I need an aggregate visibility.
I thought about nesting the class Row in the SeatingPlan
class, and making the Row.AddSeat
method private. But is it a good practice ? Because the class would have to be public and I remember having read something about it saying that we should avoid public nested classes.
c# domain-driven-design aggregate
c# domain-driven-design aggregate
edited Nov 23 '18 at 14:14
Jonathan
asked Nov 21 '18 at 13:58
JonathanJonathan
493324
493324
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
1
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39
add a comment |
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
1
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
1
1
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39
add a comment |
4 Answers
4
active
oldest
votes
Firstly I would point out that DDD is a set of guidelines, not rules. Do whatever makes sense in your situation, don't just follow DDD blindly.
That said you can use interfaces/base classes to do what you want. Here is a simple example.
public interface IRow
{
IReadOnlyList<Seat> Seats {get;}
}
public class Stadium
{
private List<Row> _rows = new List<Row>();
public IReadOnlyList<IRow> Rows => _rows;
public void AddSeat(Seat seat, int rowNum) => _rows[rowNum].AddSeat(seat);
private class Row : IRow
{
private List<Seat> _seats = new List<Seat>();
public IReadOnlyList<Seat> Seats => _seats;
public void AddSeat(Seat seat) => _seats.Add(seat);
}
}
_rows[rowNum].AddSeat(seat)
will dropIndexOutOfRange
if there are no rows orNullReference
if the list is not assigned
– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
add a comment |
Conflicting domain model role: commands vs queries
I'm suspecting that the reason you allowed external deep navigation of your aggregate root is for querying needs and that's what's causing issues. If you could avoid exposing entities outside the root then this problem goes away. You must not forget that the primary role of the domain model is to process commands while protecting invariants; not fulfilling query needs.
A single model can't be optimized for both, commands & queries. When the model is starting to fail you in one of the two roles it may be time to segregate them. That's called Command Query Responsibility Segregation (CQRS). You'd by-pass the domain model entirely for queries and go straight to the DB, after which you could get rid of most state-exposing members in your aggregate roots.
CQRS scares me...
If you dont want to go that route then you will have to live with the pains of having a single model stretched in different directions. One thing you could do to mitigate the problem you described is to use readonly interfaces, such as IRowView
which do not expose any mutating methods. Alternatively, you could also return value objects describing the state of the sub-entity, such as RowDescriptor
or RowState
. What you will start to realize however is that you will start being forced to invent new concepts that dont exist in your Ubiquitous Language, just to solve technical problems (e.g. preserve encapsulation).
Beware large aggregate roots
The Stadium
AR seems to be very large as a consistency boundary. That's usually a good indicator that the boundaries might be wrong. You shouldn't attempt to model the real world: just because a stadium contains sections which have rows, etc., in the real world doesn't mean your model should be composed that way.
Also, dont rely on the rule "A can't exist or doesn't make sense without B" to model aggregates. It's does more harm than good most of the time.
Since that's not the core of the question I'll just leave you to read this excellent Vaughn Vernon article, Effective Aggregate Design.
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, aPost
can most likely not exist without aThread
, but that doesn't mean theThread
has a collection ofPost
.
– plalx
Nov 23 '18 at 14:14
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
|
show 7 more comments
In java, I protect the access to internal objects of an aggregate by the scope of those objects. I structure the code so that each aggregate is in a package , named with aggregate name. Internal entities and value objects would be package scoped (no visibility scope keywords when defining them), so that those objects are just accessible from the same package. The aggregate root entity would be public.
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root (AR), not on other entities in the aggregate
Id rather say that an aggregate root is a consistency boundary. That's why "external objects should only call methods on the aggregate root".
On the other hand your value objects (VOs) or entities can be quite rich and encapsulate a lot of their internal rules.
E.g SeatNumber
cannot be negative, Seat
can have a method Book(Person person)
that makes sure that it's booked by only one person, Row
can have methods BookASeat(SeatNumber seatId, Person person)
and AddASeat(Seat seat)
, ...
public class Seat : Entity
{
private Person _person;
public Seat(SeatNumber id)
{
SeatId = id;
}
public SeatNumber SeatId { get; }
public void Book(Person person)
{
if(_person == person) return;
if (_person != null)
{
throw new InvalidOperationException($"Seat {SeatId} cannot be booked by {person}. {_person} already booked it.");
}
_person = person;
}
public bool IsBooked => _person != null;
}
I would create
SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to callSeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right?
But still, the
AddSeat
method ofSeatingPlan
must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to callSections[x].Rows[y].AddSeat(seatNo)
.
It's not bad to call Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
as long as Sections
is a private collection (dictionary) and SeatingPlan
doesn't expose it to an outside world.
IMHO: A disadvantage of this approach is the following - all domain rules are maintained by your aggregate root. It cam make you aggregate root too complex too understand or maintain.
In order to keep your aggregate simple I'd recommend to split into multiple entities and make each of them responsible for enforcing their own domain rules:
Row
is responsible for maintaining an internal list of its seats, has methodsAddASeat(Seat seat)
andBookASeat(SeatNumber seatId, Person person)
Section
is responsible for maintaining an internal list of rows, knows how to add an entire valid row (AddARow(Row row)
) or just to add a seat to an existing row (AddASeat(RowNumber rowId, Seat seat)
)
Stadium
(or a seat plan) can have methods likeAddASection(Section section)
,AddARow(Row row, SectionCode sectionCode)
,AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
. It all depends on the interface that you provide to your users.
You can describe your aggregate root without exposing internal collections:
/// <summary>
/// Stadium -> Sections -> Rows -> Seats
/// </summary>
public class Stadium : AggregateRoot
{
private readonly IDictionary<SectionCode, Section> _sections;
public static Stadium Create(StadiumCode id, Section sections)
{
return new Stadium(id, sections);
}
public override string Id { get; }
private Stadium(StadiumCode id, Section sections)
{
_sections = sections.ToDictionary(s => s.SectionId);
Id = id.ToString();
}
public void BookASeat(SeatNumber seat, RowNumber row, SectionCode section, Person person)
{
if (!_sections.ContainsKey(section))
{
throw new InvalidOperationException($"There is no Section {section} on a stadium {Id}.");
}
_sections[section].BookASeat(row, seat, person);
}
public void AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddASeat(rowNumber, seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddARow(row);
}
else
{
throw new InvalidOperationException();
}
}
public void AddASection(Section section)
{
if (_sections.ContainsKey(section.SectionId))
{
throw new InvalidOperationException();
}
_sections.Add(section.SectionId, section);
}
}
public abstract class AggregateRoot
{
public abstract string Id { get; }
}
public class Entity { }
public class ValueObject { }
public class SeatNumber : ValueObject { }
public class RowNumber : ValueObject { }
public class SectionCode : ValueObject { }
public class Person : ValueObject { }
public class StadiumCode : ValueObject { }
public class Row : Entity
{
private readonly IDictionary<SeatNumber, Seat> _seats;
public Row(RowNumber rowId, Seat seats)
{
RowId = rowId;
_seats = seats.ToDictionary(s => s.SeatId);
}
public RowNumber RowId { get; }
public void BookASeat(SeatNumber seatId, Person person)
{
if (!_seats.ContainsKey(seatId))
{
throw new InvalidOperationException($"There is no Seat {seatId} in row {RowId}.");
}
_seats[seatId].Book(person);
}
public bool IsBooked(SeatNumber seatId) { throw new NotImplementedException(); }
public void AddASeat(Seat seat)
{
if (_seats.ContainsKey(seat.SeatId))
{
throw new InvalidOperationException();
}
_seats.Add(seat.SeatId, seat);
}
}
public class Section : Entity
{
private readonly IDictionary<RowNumber, Row> _rows;
public Section(SectionCode sectionId, Row rows)
{
SectionId = sectionId;
_rows = rows.ToDictionary(r => r.RowId);
}
public SectionCode SectionId { get; }
public void BookASeat(RowNumber rowId, SeatNumber seatId, Person person)
{
if (!_rows.ContainsKey(rowId))
{
throw new InvalidOperationException($"There is no Row {rowId} in section {SectionId}.");
}
_rows[rowId].BookASeat(seatId, person);
}
public void AddASeat(RowNumber rowId, Seat seat)
{
_rows.TryGetValue(rowId, out var row);
if (row != null)
{
row.AddASeat(seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row)
{
if (_rows.ContainsKey(row.RowId))
{
throw new InvalidOperationException();
}
_rows.Add(row.RowId, row);
}
}
how can I prevent external objects from calling
Row.AddSeat
method, while allowing the aggregate root to call it ?
If you do not expose a Row
or Rows
as public property it automatically prevents others from calling it. E.g. in my example only Section
has access to its own private collection of _rows
and calls method AddSeat
on a single row
.
If you keep a state of the aggregate root private to itself it means that it can be changed through aggregate root methods only.
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.Row
andSection
are entities.
– Ilya Palkin
Nov 27 '18 at 16:22
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53413709%2fpreventing-external-object-to-call-methods-on-entity-inside-an-aggregate%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
Firstly I would point out that DDD is a set of guidelines, not rules. Do whatever makes sense in your situation, don't just follow DDD blindly.
That said you can use interfaces/base classes to do what you want. Here is a simple example.
public interface IRow
{
IReadOnlyList<Seat> Seats {get;}
}
public class Stadium
{
private List<Row> _rows = new List<Row>();
public IReadOnlyList<IRow> Rows => _rows;
public void AddSeat(Seat seat, int rowNum) => _rows[rowNum].AddSeat(seat);
private class Row : IRow
{
private List<Seat> _seats = new List<Seat>();
public IReadOnlyList<Seat> Seats => _seats;
public void AddSeat(Seat seat) => _seats.Add(seat);
}
}
_rows[rowNum].AddSeat(seat)
will dropIndexOutOfRange
if there are no rows orNullReference
if the list is not assigned
– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
add a comment |
Firstly I would point out that DDD is a set of guidelines, not rules. Do whatever makes sense in your situation, don't just follow DDD blindly.
That said you can use interfaces/base classes to do what you want. Here is a simple example.
public interface IRow
{
IReadOnlyList<Seat> Seats {get;}
}
public class Stadium
{
private List<Row> _rows = new List<Row>();
public IReadOnlyList<IRow> Rows => _rows;
public void AddSeat(Seat seat, int rowNum) => _rows[rowNum].AddSeat(seat);
private class Row : IRow
{
private List<Seat> _seats = new List<Seat>();
public IReadOnlyList<Seat> Seats => _seats;
public void AddSeat(Seat seat) => _seats.Add(seat);
}
}
_rows[rowNum].AddSeat(seat)
will dropIndexOutOfRange
if there are no rows orNullReference
if the list is not assigned
– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
add a comment |
Firstly I would point out that DDD is a set of guidelines, not rules. Do whatever makes sense in your situation, don't just follow DDD blindly.
That said you can use interfaces/base classes to do what you want. Here is a simple example.
public interface IRow
{
IReadOnlyList<Seat> Seats {get;}
}
public class Stadium
{
private List<Row> _rows = new List<Row>();
public IReadOnlyList<IRow> Rows => _rows;
public void AddSeat(Seat seat, int rowNum) => _rows[rowNum].AddSeat(seat);
private class Row : IRow
{
private List<Seat> _seats = new List<Seat>();
public IReadOnlyList<Seat> Seats => _seats;
public void AddSeat(Seat seat) => _seats.Add(seat);
}
}
Firstly I would point out that DDD is a set of guidelines, not rules. Do whatever makes sense in your situation, don't just follow DDD blindly.
That said you can use interfaces/base classes to do what you want. Here is a simple example.
public interface IRow
{
IReadOnlyList<Seat> Seats {get;}
}
public class Stadium
{
private List<Row> _rows = new List<Row>();
public IReadOnlyList<IRow> Rows => _rows;
public void AddSeat(Seat seat, int rowNum) => _rows[rowNum].AddSeat(seat);
private class Row : IRow
{
private List<Seat> _seats = new List<Seat>();
public IReadOnlyList<Seat> Seats => _seats;
public void AddSeat(Seat seat) => _seats.Add(seat);
}
}
edited Nov 21 '18 at 15:06
answered Nov 21 '18 at 14:08
Yair HalberstadtYair Halberstadt
1,026322
1,026322
_rows[rowNum].AddSeat(seat)
will dropIndexOutOfRange
if there are no rows orNullReference
if the list is not assigned
– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
add a comment |
_rows[rowNum].AddSeat(seat)
will dropIndexOutOfRange
if there are no rows orNullReference
if the list is not assigned
– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
_rows[rowNum].AddSeat(seat)
will drop IndexOutOfRange
if there are no rows or NullReference
if the list is not assigned– Markiian Benovskyi
Nov 21 '18 at 14:38
_rows[rowNum].AddSeat(seat)
will drop IndexOutOfRange
if there are no rows or NullReference
if the list is not assigned– Markiian Benovskyi
Nov 21 '18 at 14:38
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
I know. It was a stub of an example
– Yair Halberstadt
Nov 21 '18 at 15:04
add a comment |
Conflicting domain model role: commands vs queries
I'm suspecting that the reason you allowed external deep navigation of your aggregate root is for querying needs and that's what's causing issues. If you could avoid exposing entities outside the root then this problem goes away. You must not forget that the primary role of the domain model is to process commands while protecting invariants; not fulfilling query needs.
A single model can't be optimized for both, commands & queries. When the model is starting to fail you in one of the two roles it may be time to segregate them. That's called Command Query Responsibility Segregation (CQRS). You'd by-pass the domain model entirely for queries and go straight to the DB, after which you could get rid of most state-exposing members in your aggregate roots.
CQRS scares me...
If you dont want to go that route then you will have to live with the pains of having a single model stretched in different directions. One thing you could do to mitigate the problem you described is to use readonly interfaces, such as IRowView
which do not expose any mutating methods. Alternatively, you could also return value objects describing the state of the sub-entity, such as RowDescriptor
or RowState
. What you will start to realize however is that you will start being forced to invent new concepts that dont exist in your Ubiquitous Language, just to solve technical problems (e.g. preserve encapsulation).
Beware large aggregate roots
The Stadium
AR seems to be very large as a consistency boundary. That's usually a good indicator that the boundaries might be wrong. You shouldn't attempt to model the real world: just because a stadium contains sections which have rows, etc., in the real world doesn't mean your model should be composed that way.
Also, dont rely on the rule "A can't exist or doesn't make sense without B" to model aggregates. It's does more harm than good most of the time.
Since that's not the core of the question I'll just leave you to read this excellent Vaughn Vernon article, Effective Aggregate Design.
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, aPost
can most likely not exist without aThread
, but that doesn't mean theThread
has a collection ofPost
.
– plalx
Nov 23 '18 at 14:14
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
|
show 7 more comments
Conflicting domain model role: commands vs queries
I'm suspecting that the reason you allowed external deep navigation of your aggregate root is for querying needs and that's what's causing issues. If you could avoid exposing entities outside the root then this problem goes away. You must not forget that the primary role of the domain model is to process commands while protecting invariants; not fulfilling query needs.
A single model can't be optimized for both, commands & queries. When the model is starting to fail you in one of the two roles it may be time to segregate them. That's called Command Query Responsibility Segregation (CQRS). You'd by-pass the domain model entirely for queries and go straight to the DB, after which you could get rid of most state-exposing members in your aggregate roots.
CQRS scares me...
If you dont want to go that route then you will have to live with the pains of having a single model stretched in different directions. One thing you could do to mitigate the problem you described is to use readonly interfaces, such as IRowView
which do not expose any mutating methods. Alternatively, you could also return value objects describing the state of the sub-entity, such as RowDescriptor
or RowState
. What you will start to realize however is that you will start being forced to invent new concepts that dont exist in your Ubiquitous Language, just to solve technical problems (e.g. preserve encapsulation).
Beware large aggregate roots
The Stadium
AR seems to be very large as a consistency boundary. That's usually a good indicator that the boundaries might be wrong. You shouldn't attempt to model the real world: just because a stadium contains sections which have rows, etc., in the real world doesn't mean your model should be composed that way.
Also, dont rely on the rule "A can't exist or doesn't make sense without B" to model aggregates. It's does more harm than good most of the time.
Since that's not the core of the question I'll just leave you to read this excellent Vaughn Vernon article, Effective Aggregate Design.
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, aPost
can most likely not exist without aThread
, but that doesn't mean theThread
has a collection ofPost
.
– plalx
Nov 23 '18 at 14:14
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
|
show 7 more comments
Conflicting domain model role: commands vs queries
I'm suspecting that the reason you allowed external deep navigation of your aggregate root is for querying needs and that's what's causing issues. If you could avoid exposing entities outside the root then this problem goes away. You must not forget that the primary role of the domain model is to process commands while protecting invariants; not fulfilling query needs.
A single model can't be optimized for both, commands & queries. When the model is starting to fail you in one of the two roles it may be time to segregate them. That's called Command Query Responsibility Segregation (CQRS). You'd by-pass the domain model entirely for queries and go straight to the DB, after which you could get rid of most state-exposing members in your aggregate roots.
CQRS scares me...
If you dont want to go that route then you will have to live with the pains of having a single model stretched in different directions. One thing you could do to mitigate the problem you described is to use readonly interfaces, such as IRowView
which do not expose any mutating methods. Alternatively, you could also return value objects describing the state of the sub-entity, such as RowDescriptor
or RowState
. What you will start to realize however is that you will start being forced to invent new concepts that dont exist in your Ubiquitous Language, just to solve technical problems (e.g. preserve encapsulation).
Beware large aggregate roots
The Stadium
AR seems to be very large as a consistency boundary. That's usually a good indicator that the boundaries might be wrong. You shouldn't attempt to model the real world: just because a stadium contains sections which have rows, etc., in the real world doesn't mean your model should be composed that way.
Also, dont rely on the rule "A can't exist or doesn't make sense without B" to model aggregates. It's does more harm than good most of the time.
Since that's not the core of the question I'll just leave you to read this excellent Vaughn Vernon article, Effective Aggregate Design.
Conflicting domain model role: commands vs queries
I'm suspecting that the reason you allowed external deep navigation of your aggregate root is for querying needs and that's what's causing issues. If you could avoid exposing entities outside the root then this problem goes away. You must not forget that the primary role of the domain model is to process commands while protecting invariants; not fulfilling query needs.
A single model can't be optimized for both, commands & queries. When the model is starting to fail you in one of the two roles it may be time to segregate them. That's called Command Query Responsibility Segregation (CQRS). You'd by-pass the domain model entirely for queries and go straight to the DB, after which you could get rid of most state-exposing members in your aggregate roots.
CQRS scares me...
If you dont want to go that route then you will have to live with the pains of having a single model stretched in different directions. One thing you could do to mitigate the problem you described is to use readonly interfaces, such as IRowView
which do not expose any mutating methods. Alternatively, you could also return value objects describing the state of the sub-entity, such as RowDescriptor
or RowState
. What you will start to realize however is that you will start being forced to invent new concepts that dont exist in your Ubiquitous Language, just to solve technical problems (e.g. preserve encapsulation).
Beware large aggregate roots
The Stadium
AR seems to be very large as a consistency boundary. That's usually a good indicator that the boundaries might be wrong. You shouldn't attempt to model the real world: just because a stadium contains sections which have rows, etc., in the real world doesn't mean your model should be composed that way.
Also, dont rely on the rule "A can't exist or doesn't make sense without B" to model aggregates. It's does more harm than good most of the time.
Since that's not the core of the question I'll just leave you to read this excellent Vaughn Vernon article, Effective Aggregate Design.
answered Nov 22 '18 at 15:02
plalxplalx
32.3k44770
32.3k44770
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, aPost
can most likely not exist without aThread
, but that doesn't mean theThread
has a collection ofPost
.
– plalx
Nov 23 '18 at 14:14
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
|
show 7 more comments
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, aPost
can most likely not exist without aThread
, but that doesn't mean theThread
has a collection ofPost
.
– plalx
Nov 23 '18 at 14:14
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
I like the first read of your answer, i already heard of CQRS, i can't say i like it either but i will look deeper at it. There's a thing you said that made me frown a little: "You shouldn't attempt to model the real world"... Isn't DDD about "modeling the real world (from the domain point of view at least)" ??
– Jonathan
Nov 22 '18 at 22:14
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
@Jonathan Why don't you like CQRS? You don't need a secondary datastore nor you have to use event sourcing for CQRS. It can be as simple as querying the written state of your AR from the DB directly. It gives much more flexibility and allows you to keep your ARs simple and free of unnecessary state-exposing methods and relationships.
– plalx
Nov 23 '18 at 14:07
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
I thought of CQRS more for queries that would span data of multiple aggregates into some report/list rather than only for the purpose of querying and display one aggregte's state. Also in my example I changed some names but maybe I should'nt because indeed "Statium" would be too big for a single aggregate. A better name would have been a SeatingPlan
– Jonathan
Nov 23 '18 at 14:10
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, a
Post
can most likely not exist without a Thread
, but that doesn't mean the Thread
has a collection of Post
.– plalx
Nov 23 '18 at 14:14
@Jonathan Also, when I say don't model the real world, I'm mainly saying: don't get fooled into composing objects together just because that's how they physically are in the real world or you will end up with very large impractical ARs that will cause both, contention issues and performance issues. It's also not because something can't exist without something else that they should be part of the same AR. For instance, a
Post
can most likely not exist without a Thread
, but that doesn't mean the Thread
has a collection of Post
.– plalx
Nov 23 '18 at 14:14
1
1
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
For every use case, identify which data needs to be changed and which data needs to be checked in order to validate invariants. That should set your initial AR boundaries. However, you may still end up with impractical ARs due to concurrency. At that point you have to challenge the rules and see if you can further break the AR and make some of it's parts eventually consistent.
– plalx
Nov 23 '18 at 14:16
|
show 7 more comments
In java, I protect the access to internal objects of an aggregate by the scope of those objects. I structure the code so that each aggregate is in a package , named with aggregate name. Internal entities and value objects would be package scoped (no visibility scope keywords when defining them), so that those objects are just accessible from the same package. The aggregate root entity would be public.
add a comment |
In java, I protect the access to internal objects of an aggregate by the scope of those objects. I structure the code so that each aggregate is in a package , named with aggregate name. Internal entities and value objects would be package scoped (no visibility scope keywords when defining them), so that those objects are just accessible from the same package. The aggregate root entity would be public.
add a comment |
In java, I protect the access to internal objects of an aggregate by the scope of those objects. I structure the code so that each aggregate is in a package , named with aggregate name. Internal entities and value objects would be package scoped (no visibility scope keywords when defining them), so that those objects are just accessible from the same package. The aggregate root entity would be public.
In java, I protect the access to internal objects of an aggregate by the scope of those objects. I structure the code so that each aggregate is in a package , named with aggregate name. Internal entities and value objects would be package scoped (no visibility scope keywords when defining them), so that those objects are just accessible from the same package. The aggregate root entity would be public.
answered Nov 25 '18 at 14:31
choquero70choquero70
1,23611631
1,23611631
add a comment |
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root (AR), not on other entities in the aggregate
Id rather say that an aggregate root is a consistency boundary. That's why "external objects should only call methods on the aggregate root".
On the other hand your value objects (VOs) or entities can be quite rich and encapsulate a lot of their internal rules.
E.g SeatNumber
cannot be negative, Seat
can have a method Book(Person person)
that makes sure that it's booked by only one person, Row
can have methods BookASeat(SeatNumber seatId, Person person)
and AddASeat(Seat seat)
, ...
public class Seat : Entity
{
private Person _person;
public Seat(SeatNumber id)
{
SeatId = id;
}
public SeatNumber SeatId { get; }
public void Book(Person person)
{
if(_person == person) return;
if (_person != null)
{
throw new InvalidOperationException($"Seat {SeatId} cannot be booked by {person}. {_person} already booked it.");
}
_person = person;
}
public bool IsBooked => _person != null;
}
I would create
SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to callSeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right?
But still, the
AddSeat
method ofSeatingPlan
must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to callSections[x].Rows[y].AddSeat(seatNo)
.
It's not bad to call Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
as long as Sections
is a private collection (dictionary) and SeatingPlan
doesn't expose it to an outside world.
IMHO: A disadvantage of this approach is the following - all domain rules are maintained by your aggregate root. It cam make you aggregate root too complex too understand or maintain.
In order to keep your aggregate simple I'd recommend to split into multiple entities and make each of them responsible for enforcing their own domain rules:
Row
is responsible for maintaining an internal list of its seats, has methodsAddASeat(Seat seat)
andBookASeat(SeatNumber seatId, Person person)
Section
is responsible for maintaining an internal list of rows, knows how to add an entire valid row (AddARow(Row row)
) or just to add a seat to an existing row (AddASeat(RowNumber rowId, Seat seat)
)
Stadium
(or a seat plan) can have methods likeAddASection(Section section)
,AddARow(Row row, SectionCode sectionCode)
,AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
. It all depends on the interface that you provide to your users.
You can describe your aggregate root without exposing internal collections:
/// <summary>
/// Stadium -> Sections -> Rows -> Seats
/// </summary>
public class Stadium : AggregateRoot
{
private readonly IDictionary<SectionCode, Section> _sections;
public static Stadium Create(StadiumCode id, Section sections)
{
return new Stadium(id, sections);
}
public override string Id { get; }
private Stadium(StadiumCode id, Section sections)
{
_sections = sections.ToDictionary(s => s.SectionId);
Id = id.ToString();
}
public void BookASeat(SeatNumber seat, RowNumber row, SectionCode section, Person person)
{
if (!_sections.ContainsKey(section))
{
throw new InvalidOperationException($"There is no Section {section} on a stadium {Id}.");
}
_sections[section].BookASeat(row, seat, person);
}
public void AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddASeat(rowNumber, seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddARow(row);
}
else
{
throw new InvalidOperationException();
}
}
public void AddASection(Section section)
{
if (_sections.ContainsKey(section.SectionId))
{
throw new InvalidOperationException();
}
_sections.Add(section.SectionId, section);
}
}
public abstract class AggregateRoot
{
public abstract string Id { get; }
}
public class Entity { }
public class ValueObject { }
public class SeatNumber : ValueObject { }
public class RowNumber : ValueObject { }
public class SectionCode : ValueObject { }
public class Person : ValueObject { }
public class StadiumCode : ValueObject { }
public class Row : Entity
{
private readonly IDictionary<SeatNumber, Seat> _seats;
public Row(RowNumber rowId, Seat seats)
{
RowId = rowId;
_seats = seats.ToDictionary(s => s.SeatId);
}
public RowNumber RowId { get; }
public void BookASeat(SeatNumber seatId, Person person)
{
if (!_seats.ContainsKey(seatId))
{
throw new InvalidOperationException($"There is no Seat {seatId} in row {RowId}.");
}
_seats[seatId].Book(person);
}
public bool IsBooked(SeatNumber seatId) { throw new NotImplementedException(); }
public void AddASeat(Seat seat)
{
if (_seats.ContainsKey(seat.SeatId))
{
throw new InvalidOperationException();
}
_seats.Add(seat.SeatId, seat);
}
}
public class Section : Entity
{
private readonly IDictionary<RowNumber, Row> _rows;
public Section(SectionCode sectionId, Row rows)
{
SectionId = sectionId;
_rows = rows.ToDictionary(r => r.RowId);
}
public SectionCode SectionId { get; }
public void BookASeat(RowNumber rowId, SeatNumber seatId, Person person)
{
if (!_rows.ContainsKey(rowId))
{
throw new InvalidOperationException($"There is no Row {rowId} in section {SectionId}.");
}
_rows[rowId].BookASeat(seatId, person);
}
public void AddASeat(RowNumber rowId, Seat seat)
{
_rows.TryGetValue(rowId, out var row);
if (row != null)
{
row.AddASeat(seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row)
{
if (_rows.ContainsKey(row.RowId))
{
throw new InvalidOperationException();
}
_rows.Add(row.RowId, row);
}
}
how can I prevent external objects from calling
Row.AddSeat
method, while allowing the aggregate root to call it ?
If you do not expose a Row
or Rows
as public property it automatically prevents others from calling it. E.g. in my example only Section
has access to its own private collection of _rows
and calls method AddSeat
on a single row
.
If you keep a state of the aggregate root private to itself it means that it can be changed through aggregate root methods only.
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.Row
andSection
are entities.
– Ilya Palkin
Nov 27 '18 at 16:22
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root (AR), not on other entities in the aggregate
Id rather say that an aggregate root is a consistency boundary. That's why "external objects should only call methods on the aggregate root".
On the other hand your value objects (VOs) or entities can be quite rich and encapsulate a lot of their internal rules.
E.g SeatNumber
cannot be negative, Seat
can have a method Book(Person person)
that makes sure that it's booked by only one person, Row
can have methods BookASeat(SeatNumber seatId, Person person)
and AddASeat(Seat seat)
, ...
public class Seat : Entity
{
private Person _person;
public Seat(SeatNumber id)
{
SeatId = id;
}
public SeatNumber SeatId { get; }
public void Book(Person person)
{
if(_person == person) return;
if (_person != null)
{
throw new InvalidOperationException($"Seat {SeatId} cannot be booked by {person}. {_person} already booked it.");
}
_person = person;
}
public bool IsBooked => _person != null;
}
I would create
SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to callSeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right?
But still, the
AddSeat
method ofSeatingPlan
must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to callSections[x].Rows[y].AddSeat(seatNo)
.
It's not bad to call Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
as long as Sections
is a private collection (dictionary) and SeatingPlan
doesn't expose it to an outside world.
IMHO: A disadvantage of this approach is the following - all domain rules are maintained by your aggregate root. It cam make you aggregate root too complex too understand or maintain.
In order to keep your aggregate simple I'd recommend to split into multiple entities and make each of them responsible for enforcing their own domain rules:
Row
is responsible for maintaining an internal list of its seats, has methodsAddASeat(Seat seat)
andBookASeat(SeatNumber seatId, Person person)
Section
is responsible for maintaining an internal list of rows, knows how to add an entire valid row (AddARow(Row row)
) or just to add a seat to an existing row (AddASeat(RowNumber rowId, Seat seat)
)
Stadium
(or a seat plan) can have methods likeAddASection(Section section)
,AddARow(Row row, SectionCode sectionCode)
,AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
. It all depends on the interface that you provide to your users.
You can describe your aggregate root without exposing internal collections:
/// <summary>
/// Stadium -> Sections -> Rows -> Seats
/// </summary>
public class Stadium : AggregateRoot
{
private readonly IDictionary<SectionCode, Section> _sections;
public static Stadium Create(StadiumCode id, Section sections)
{
return new Stadium(id, sections);
}
public override string Id { get; }
private Stadium(StadiumCode id, Section sections)
{
_sections = sections.ToDictionary(s => s.SectionId);
Id = id.ToString();
}
public void BookASeat(SeatNumber seat, RowNumber row, SectionCode section, Person person)
{
if (!_sections.ContainsKey(section))
{
throw new InvalidOperationException($"There is no Section {section} on a stadium {Id}.");
}
_sections[section].BookASeat(row, seat, person);
}
public void AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddASeat(rowNumber, seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddARow(row);
}
else
{
throw new InvalidOperationException();
}
}
public void AddASection(Section section)
{
if (_sections.ContainsKey(section.SectionId))
{
throw new InvalidOperationException();
}
_sections.Add(section.SectionId, section);
}
}
public abstract class AggregateRoot
{
public abstract string Id { get; }
}
public class Entity { }
public class ValueObject { }
public class SeatNumber : ValueObject { }
public class RowNumber : ValueObject { }
public class SectionCode : ValueObject { }
public class Person : ValueObject { }
public class StadiumCode : ValueObject { }
public class Row : Entity
{
private readonly IDictionary<SeatNumber, Seat> _seats;
public Row(RowNumber rowId, Seat seats)
{
RowId = rowId;
_seats = seats.ToDictionary(s => s.SeatId);
}
public RowNumber RowId { get; }
public void BookASeat(SeatNumber seatId, Person person)
{
if (!_seats.ContainsKey(seatId))
{
throw new InvalidOperationException($"There is no Seat {seatId} in row {RowId}.");
}
_seats[seatId].Book(person);
}
public bool IsBooked(SeatNumber seatId) { throw new NotImplementedException(); }
public void AddASeat(Seat seat)
{
if (_seats.ContainsKey(seat.SeatId))
{
throw new InvalidOperationException();
}
_seats.Add(seat.SeatId, seat);
}
}
public class Section : Entity
{
private readonly IDictionary<RowNumber, Row> _rows;
public Section(SectionCode sectionId, Row rows)
{
SectionId = sectionId;
_rows = rows.ToDictionary(r => r.RowId);
}
public SectionCode SectionId { get; }
public void BookASeat(RowNumber rowId, SeatNumber seatId, Person person)
{
if (!_rows.ContainsKey(rowId))
{
throw new InvalidOperationException($"There is no Row {rowId} in section {SectionId}.");
}
_rows[rowId].BookASeat(seatId, person);
}
public void AddASeat(RowNumber rowId, Seat seat)
{
_rows.TryGetValue(rowId, out var row);
if (row != null)
{
row.AddASeat(seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row)
{
if (_rows.ContainsKey(row.RowId))
{
throw new InvalidOperationException();
}
_rows.Add(row.RowId, row);
}
}
how can I prevent external objects from calling
Row.AddSeat
method, while allowing the aggregate root to call it ?
If you do not expose a Row
or Rows
as public property it automatically prevents others from calling it. E.g. in my example only Section
has access to its own private collection of _rows
and calls method AddSeat
on a single row
.
If you keep a state of the aggregate root private to itself it means that it can be changed through aggregate root methods only.
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.Row
andSection
are entities.
– Ilya Palkin
Nov 27 '18 at 16:22
add a comment |
According to DDD principles, external objects should only call methods on an aggregate root (AR), not on other entities in the aggregate
Id rather say that an aggregate root is a consistency boundary. That's why "external objects should only call methods on the aggregate root".
On the other hand your value objects (VOs) or entities can be quite rich and encapsulate a lot of their internal rules.
E.g SeatNumber
cannot be negative, Seat
can have a method Book(Person person)
that makes sure that it's booked by only one person, Row
can have methods BookASeat(SeatNumber seatId, Person person)
and AddASeat(Seat seat)
, ...
public class Seat : Entity
{
private Person _person;
public Seat(SeatNumber id)
{
SeatId = id;
}
public SeatNumber SeatId { get; }
public void Book(Person person)
{
if(_person == person) return;
if (_person != null)
{
throw new InvalidOperationException($"Seat {SeatId} cannot be booked by {person}. {_person} already booked it.");
}
_person = person;
}
public bool IsBooked => _person != null;
}
I would create
SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to callSeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right?
But still, the
AddSeat
method ofSeatingPlan
must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to callSections[x].Rows[y].AddSeat(seatNo)
.
It's not bad to call Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
as long as Sections
is a private collection (dictionary) and SeatingPlan
doesn't expose it to an outside world.
IMHO: A disadvantage of this approach is the following - all domain rules are maintained by your aggregate root. It cam make you aggregate root too complex too understand or maintain.
In order to keep your aggregate simple I'd recommend to split into multiple entities and make each of them responsible for enforcing their own domain rules:
Row
is responsible for maintaining an internal list of its seats, has methodsAddASeat(Seat seat)
andBookASeat(SeatNumber seatId, Person person)
Section
is responsible for maintaining an internal list of rows, knows how to add an entire valid row (AddARow(Row row)
) or just to add a seat to an existing row (AddASeat(RowNumber rowId, Seat seat)
)
Stadium
(or a seat plan) can have methods likeAddASection(Section section)
,AddARow(Row row, SectionCode sectionCode)
,AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
. It all depends on the interface that you provide to your users.
You can describe your aggregate root without exposing internal collections:
/// <summary>
/// Stadium -> Sections -> Rows -> Seats
/// </summary>
public class Stadium : AggregateRoot
{
private readonly IDictionary<SectionCode, Section> _sections;
public static Stadium Create(StadiumCode id, Section sections)
{
return new Stadium(id, sections);
}
public override string Id { get; }
private Stadium(StadiumCode id, Section sections)
{
_sections = sections.ToDictionary(s => s.SectionId);
Id = id.ToString();
}
public void BookASeat(SeatNumber seat, RowNumber row, SectionCode section, Person person)
{
if (!_sections.ContainsKey(section))
{
throw new InvalidOperationException($"There is no Section {section} on a stadium {Id}.");
}
_sections[section].BookASeat(row, seat, person);
}
public void AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddASeat(rowNumber, seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddARow(row);
}
else
{
throw new InvalidOperationException();
}
}
public void AddASection(Section section)
{
if (_sections.ContainsKey(section.SectionId))
{
throw new InvalidOperationException();
}
_sections.Add(section.SectionId, section);
}
}
public abstract class AggregateRoot
{
public abstract string Id { get; }
}
public class Entity { }
public class ValueObject { }
public class SeatNumber : ValueObject { }
public class RowNumber : ValueObject { }
public class SectionCode : ValueObject { }
public class Person : ValueObject { }
public class StadiumCode : ValueObject { }
public class Row : Entity
{
private readonly IDictionary<SeatNumber, Seat> _seats;
public Row(RowNumber rowId, Seat seats)
{
RowId = rowId;
_seats = seats.ToDictionary(s => s.SeatId);
}
public RowNumber RowId { get; }
public void BookASeat(SeatNumber seatId, Person person)
{
if (!_seats.ContainsKey(seatId))
{
throw new InvalidOperationException($"There is no Seat {seatId} in row {RowId}.");
}
_seats[seatId].Book(person);
}
public bool IsBooked(SeatNumber seatId) { throw new NotImplementedException(); }
public void AddASeat(Seat seat)
{
if (_seats.ContainsKey(seat.SeatId))
{
throw new InvalidOperationException();
}
_seats.Add(seat.SeatId, seat);
}
}
public class Section : Entity
{
private readonly IDictionary<RowNumber, Row> _rows;
public Section(SectionCode sectionId, Row rows)
{
SectionId = sectionId;
_rows = rows.ToDictionary(r => r.RowId);
}
public SectionCode SectionId { get; }
public void BookASeat(RowNumber rowId, SeatNumber seatId, Person person)
{
if (!_rows.ContainsKey(rowId))
{
throw new InvalidOperationException($"There is no Row {rowId} in section {SectionId}.");
}
_rows[rowId].BookASeat(seatId, person);
}
public void AddASeat(RowNumber rowId, Seat seat)
{
_rows.TryGetValue(rowId, out var row);
if (row != null)
{
row.AddASeat(seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row)
{
if (_rows.ContainsKey(row.RowId))
{
throw new InvalidOperationException();
}
_rows.Add(row.RowId, row);
}
}
how can I prevent external objects from calling
Row.AddSeat
method, while allowing the aggregate root to call it ?
If you do not expose a Row
or Rows
as public property it automatically prevents others from calling it. E.g. in my example only Section
has access to its own private collection of _rows
and calls method AddSeat
on a single row
.
If you keep a state of the aggregate root private to itself it means that it can be changed through aggregate root methods only.
According to DDD principles, external objects should only call methods on an aggregate root (AR), not on other entities in the aggregate
Id rather say that an aggregate root is a consistency boundary. That's why "external objects should only call methods on the aggregate root".
On the other hand your value objects (VOs) or entities can be quite rich and encapsulate a lot of their internal rules.
E.g SeatNumber
cannot be negative, Seat
can have a method Book(Person person)
that makes sure that it's booked by only one person, Row
can have methods BookASeat(SeatNumber seatId, Person person)
and AddASeat(Seat seat)
, ...
public class Seat : Entity
{
private Person _person;
public Seat(SeatNumber id)
{
SeatId = id;
}
public SeatNumber SeatId { get; }
public void Book(Person person)
{
if(_person == person) return;
if (_person != null)
{
throw new InvalidOperationException($"Seat {SeatId} cannot be booked by {person}. {_person} already booked it.");
}
_person = person;
}
public bool IsBooked => _person != null;
}
I would create
SeatingPlan.AddSeat(sectionId, rowId, seatNo)
in order to prevent external objects to callSeatingPlan.Sections[x].Rows[y].Seat[s].Add
, which is bad, right?
But still, the
AddSeat
method ofSeatingPlan
must delegate the seat creation to the row object, because the seat is a composite of the row, the row owns the seats. So it has to callSections[x].Rows[y].AddSeat(seatNo)
.
It's not bad to call Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
as long as Sections
is a private collection (dictionary) and SeatingPlan
doesn't expose it to an outside world.
IMHO: A disadvantage of this approach is the following - all domain rules are maintained by your aggregate root. It cam make you aggregate root too complex too understand or maintain.
In order to keep your aggregate simple I'd recommend to split into multiple entities and make each of them responsible for enforcing their own domain rules:
Row
is responsible for maintaining an internal list of its seats, has methodsAddASeat(Seat seat)
andBookASeat(SeatNumber seatId, Person person)
Section
is responsible for maintaining an internal list of rows, knows how to add an entire valid row (AddARow(Row row)
) or just to add a seat to an existing row (AddASeat(RowNumber rowId, Seat seat)
)
Stadium
(or a seat plan) can have methods likeAddASection(Section section)
,AddARow(Row row, SectionCode sectionCode)
,AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
. It all depends on the interface that you provide to your users.
You can describe your aggregate root without exposing internal collections:
/// <summary>
/// Stadium -> Sections -> Rows -> Seats
/// </summary>
public class Stadium : AggregateRoot
{
private readonly IDictionary<SectionCode, Section> _sections;
public static Stadium Create(StadiumCode id, Section sections)
{
return new Stadium(id, sections);
}
public override string Id { get; }
private Stadium(StadiumCode id, Section sections)
{
_sections = sections.ToDictionary(s => s.SectionId);
Id = id.ToString();
}
public void BookASeat(SeatNumber seat, RowNumber row, SectionCode section, Person person)
{
if (!_sections.ContainsKey(section))
{
throw new InvalidOperationException($"There is no Section {section} on a stadium {Id}.");
}
_sections[section].BookASeat(row, seat, person);
}
public void AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddASeat(rowNumber, seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row, SectionCode sectionCode)
{
_sections.TryGetValue(sectionCode, out var section);
if (section != null)
{
section.AddARow(row);
}
else
{
throw new InvalidOperationException();
}
}
public void AddASection(Section section)
{
if (_sections.ContainsKey(section.SectionId))
{
throw new InvalidOperationException();
}
_sections.Add(section.SectionId, section);
}
}
public abstract class AggregateRoot
{
public abstract string Id { get; }
}
public class Entity { }
public class ValueObject { }
public class SeatNumber : ValueObject { }
public class RowNumber : ValueObject { }
public class SectionCode : ValueObject { }
public class Person : ValueObject { }
public class StadiumCode : ValueObject { }
public class Row : Entity
{
private readonly IDictionary<SeatNumber, Seat> _seats;
public Row(RowNumber rowId, Seat seats)
{
RowId = rowId;
_seats = seats.ToDictionary(s => s.SeatId);
}
public RowNumber RowId { get; }
public void BookASeat(SeatNumber seatId, Person person)
{
if (!_seats.ContainsKey(seatId))
{
throw new InvalidOperationException($"There is no Seat {seatId} in row {RowId}.");
}
_seats[seatId].Book(person);
}
public bool IsBooked(SeatNumber seatId) { throw new NotImplementedException(); }
public void AddASeat(Seat seat)
{
if (_seats.ContainsKey(seat.SeatId))
{
throw new InvalidOperationException();
}
_seats.Add(seat.SeatId, seat);
}
}
public class Section : Entity
{
private readonly IDictionary<RowNumber, Row> _rows;
public Section(SectionCode sectionId, Row rows)
{
SectionId = sectionId;
_rows = rows.ToDictionary(r => r.RowId);
}
public SectionCode SectionId { get; }
public void BookASeat(RowNumber rowId, SeatNumber seatId, Person person)
{
if (!_rows.ContainsKey(rowId))
{
throw new InvalidOperationException($"There is no Row {rowId} in section {SectionId}.");
}
_rows[rowId].BookASeat(seatId, person);
}
public void AddASeat(RowNumber rowId, Seat seat)
{
_rows.TryGetValue(rowId, out var row);
if (row != null)
{
row.AddASeat(seat);
}
else
{
throw new InvalidOperationException();
}
}
public void AddARow(Row row)
{
if (_rows.ContainsKey(row.RowId))
{
throw new InvalidOperationException();
}
_rows.Add(row.RowId, row);
}
}
how can I prevent external objects from calling
Row.AddSeat
method, while allowing the aggregate root to call it ?
If you do not expose a Row
or Rows
as public property it automatically prevents others from calling it. E.g. in my example only Section
has access to its own private collection of _rows
and calls method AddSeat
on a single row
.
If you keep a state of the aggregate root private to itself it means that it can be changed through aggregate root methods only.
edited Nov 27 '18 at 16:22
answered Nov 26 '18 at 20:18
Ilya PalkinIlya Palkin
7,27921834
7,27921834
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.Row
andSection
are entities.
– Ilya Palkin
Nov 27 '18 at 16:22
add a comment |
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.Row
andSection
are entities.
– Ilya Palkin
Nov 27 '18 at 16:22
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Something is wrong in your code, you define Row and Section as value objects but have methodes that changes their state.
– Jonathan
Nov 27 '18 at 13:07
Yeah, just a typo.
Row
and Section
are entities.– Ilya Palkin
Nov 27 '18 at 16:22
Yeah, just a typo.
Row
and Section
are entities.– Ilya Palkin
Nov 27 '18 at 16:22
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53413709%2fpreventing-external-object-to-call-methods-on-entity-inside-an-aggregate%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Two interfaces, a read-only interface and a full interface. Expose the readonly interface to users, use the full interface internally.
– spender
Nov 21 '18 at 14:10
If you are developing this as an assembly then there is an ‘internal’ keyword
– Markiian Benovskyi
Nov 21 '18 at 17:31
1
Why do you need to expose entities to the outside in the first place? For queries? Don't use the domain model for queries and this problem most likely goes away ;)
– plalx
Nov 21 '18 at 17:39