Предотвращение внешнего объекта для вызова методов на объекте внутри агрегата - PullRequest
0 голосов
/ 21 ноября 2018

Согласно принципам DDD, внешние объекты должны вызывать методы только в корне совокупности, а не в других сущностях в совокупности, верно?

В случае вложенных сущностей, например: SeatingPlan -> Sections -> Rows -> Seats

SeatingPlan - это совокупный корень, а секции, строки и места - это объекты, которые не имеют смысла вне его родительской сущности.


Допустим, я хочу добавить места в плане рассадки.

Я бы создал SeatingPlan.AddSeat(sectionId, rowId, seatNo), чтобы внешние объекты не вызывали SeatingPlan.Sections[x].Rows[y].Seat[s].Add, что плохо, верно?

Но все же, метод AddSeat SeatingPlan должен делегировать создание места в строкеобъект, потому что место является составной частью ряда, ряд принадлежит местам.Поэтому он должен вызвать Sections[x].Rows[y].AddSeat(seatNo).


Теперь у меня вопрос , как я могу предотвратить вызов внешних методов Row.AddSeat, при этом позволяя агрегатному корню вызывать его?

внутренняя видимость слишком велика, даже видимость пространства имен (при условии, что она существует даже в c #) будет слишком большой.Мне нужна совокупная видимость.

Я думал о вложении класса Row в класс SeatingPlan и о том, чтобы сделать метод Row.AddSeat закрытым. Но разве это хорошая практика? Поскольку класс должен быть общедоступным, и я помню, что прочитал что-то об этом, сказав, что мы должны избегать открытых вложенных классов.

Ответы [ 4 ]

0 голосов
/ 26 ноября 2018

В соответствии с принципами 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.

Если вы сохраняете состояние агрегатного корневого элемента закрытым для себя, это означает, что его можно изменить только с помощью агрегатного корневого метода.

0 голосов
/ 22 ноября 2018

Противоречивая роль модели домена: команды против запросов

Я подозреваю, что причина, по которой вы разрешили внешнюю глубокую навигацию по вашему сводному корню, заключается в запросах потребностей, и именно это вызывает проблемы.Если вы можете избежать показа сущностей вне корня, то эта проблема исчезнет.Вы не должны забывать, что основная роль модели предметной области заключается в обработке команд при защите инвариантов;не удовлетворяет запросам.

Одна модель не может быть оптимизирована как для команд, так и для запросов.Когда модель начинает подводить вас в одной из двух ролей, возможно, пришло время разделить их.Это называется Разделение ответственности по запросам команд (CQRS) .Вы полностью обойдете модель предметной области для запросов и сразу перейдете к БД, после чего сможете избавиться от большинства элементов, представляющих состояние, в ваших совокупных корнях.

CQRS пугает меня ...

Если вы не хотите идти по этому пути, вам придется пережить трудности, связанные с растяжкой одной модели в разных направлениях.Единственное, что вы можете сделать, чтобы смягчить проблему, которую вы описали, - это использовать интерфейсы только для чтения, такие как IRowView, которые не предоставляют никаких методов мутации.В качестве альтернативы вы также можете возвращать объекты-значения, описывающие состояние подобъекта, например RowDescriptor или RowState.Однако вы начнете понимать, что вы будете вынуждены изобретать новые концепции, которых нет в вашем вездесущем языке, просто для решения технических проблем (например, сохранения инкапсуляции).

Остерегайтесь больших совокупных корней

AR Stadium кажется очень большим как граница согласованности.Обычно это хороший показатель того, что границы могут быть неправильными.Вы не должны пытаться моделировать реальный мир: просто потому, что стадион содержит секции, в которых есть ряды и т. Д., В реальном мире не означает, что ваша модель должна быть составлена ​​таким образом.

Кроме того, не полагайтесьпо правилу «А не может существовать или не имеет смысла без В» для моделирования агрегатов.Большую часть времени это приносит больше вреда, чем пользы.

Поскольку это не является ключевым вопросом вопроса, я просто оставлю вас прочитать эту превосходную статью Вона Вернона, Эффективный совокупный дизайн .

0 голосов
/ 25 ноября 2018

В Java я защищаю доступ к внутренним объектам агрегата областью действия этих объектов.Я структурирую код так, чтобы каждый агрегат находился в пакете с именем агрегата.Внутренние сущности и объекты значений будут иметь область видимости пакета (без определения ключевых слов области видимости при их определении), так что эти объекты будут просто доступны из одного и того же пакета.Совокупный корневой объект будет общедоступным.

0 голосов
/ 21 ноября 2018

Во-первых, я бы отметил, что DDD - это набор правил, а не правил.Делайте все, что имеет смысл в вашей ситуации, а не просто следуйте DDD вслепую.

При этом вы можете использовать интерфейсы / базовые классы, чтобы делать то, что вы хотите.Вот простой пример.

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);
    }
}
...