В соответствии с принципами DDD внешние объекты должны вызывать методы только в совокупном корне (AR), а не в других объектах в совокупности
Я бы скорее сказал, что совокупный корень являетсяграница согласованности.Вот почему «внешние объекты должны вызывать методы только в совокупном корне».
С другой стороны, ваши ценностные объекты (VO) или сущности могут быть достаточно богатыми и инкапсулировать множество своих внутренних правил.
Например, SeatNumber
не может быть отрицательным, Seat
может иметь метод Book(Person person)
, который гарантирует, что он забронирован только одним человеком, Row
может иметь методы BookASeat(SeatNumber seatId, Person person)
и 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;
}
Я бы создал SeatingPlan.AddSeat(sectionId, rowId, seatNo)
, чтобы внешние объекты не вызывали SeatingPlan.Sections[x].Rows[y].Seat[s].Add
, что плохо, верно?
Но все же, AddSeat
метод SeatingPlan
должен делегировать создание места для объекта строки, потому что место является составной частью ряда, а ряд владеет местами.Поэтому он должен вызывать Sections[x].Rows[y].AddSeat(seatNo)
.
. Неплохо называть Sections[sectionNumber].Rows[rowNo].Seat[seat.SeatNo].Add(seat)
, если Sections
является частной коллекцией (словарем), а SeatingPlan
не предоставляет еговнешний мир.
ИМХО: Недостаток этого подхода заключается в следующем: все правила домена поддерживаются вашим совокупным корнем.Это может сделать вас слишком сложным для понимания или поддержки.
Чтобы упростить вашу совокупность, я бы порекомендовал разделить ее на несколько сущностей и сделать так, чтобы каждая из них отвечала за соблюдение своих собственных правил домена:
Row
отвечает заподдерживает внутренний список своих мест, имеет методы AddASeat(Seat seat)
и BookASeat(SeatNumber seatId, Person person)
Section
, отвечает за ведение внутреннего списка строк, знает, как добавить всю действительную строку (AddARow(Row row)
) илипросто для добавления места в существующий ряд (AddASeat(RowNumber rowId, Seat seat)
) Stadium
(или план мест) можно использовать такие методы, как AddASection(Section section)
, AddARow(Row row, SectionCode sectionCode)
, AddASeat(Seat seat, RowNumber rowNumber, SectionCode sectionCode)
.Все зависит от интерфейса, который вы предоставляете своим пользователям.
Вы можете описать свой сводный корень, не раскрывая внутренние коллекции:
/// <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);
}
}
как я могу предотвратить внешниеобъекты от вызова метода Row.AddSeat
, при этом позволяя совокупному корню вызывать его?
Если вы не выставите Row
или Rows
в качестве открытого свойства, оно автоматически запрещает другим вызывать его.Например, в моем примере только Section
имеет доступ к собственной частной коллекции _rows
и вызывает метод AddSeat
для одного row
.
Если вы сохраняете состояние агрегатного корневого элемента закрытым для себя, это означает, что его можно изменить только с помощью агрегатного корневого метода.