DDD: разные коллекции внутри и снаружи агрегата
От: zelenprog  
Дата: 17.06.24 07:05
Оценка:
Здравствуйте!

Продолжаю разбираться с DDD.

Возник такой вопрос по коллекциям.
Агрегат Tovar имеет свойства-коллекции AnalogList, PackList.
Проблема в следующем: клиенты объекта Tovar при работе с этими коллекциями не должны иметь возможности их изменять.
То есть, предоставляемые агрегатом коллекции извне должны быть доступны только на чтение. Значит, следующий код не должен компилироваться:

lTovar = lRepository.GetTovar(lTovarID);
lNewAnalog = new TovarAnalog(...);
lTovar.AnalogList.Add(lNewAnalog); // здесь должна быть ошибка: метод Add не существует


Однако, при формировании агрегата в эти коллекции нужно же как-то добавлять элементы.
То есть эти же самые коллекции, чтобы их можно было заполнить, должны иметь метод Add. Но этот метод дожен быть доступен только при формированиии коллекции.
Получается, мы имеем противоречие.

Как это правильно реализовать в программном коде?

Похоже, что нужно использовать два разных класса коллекций.
Один класс должен быть "полноценным" (то есть иметь возможность добавления\удаления элементов), а второй класс коллекции — только на чтение и поиск элементов.
И оба эти класса должны "разделять" общий набор элементов.
Верно, я понимаю?
Re: DDD: разные коллекции внутри и снаружи агрегата
От: fmiracle  
Дата: 17.06.24 07:24
Оценка:
Здравствуйте, zelenprog, Вы писали:

Z>
Z>lTovar = lRepository.GetTovar(lTovarID);
Z>lNewAnalog = new TovarAnalog(...);
Z>lTovar.AnalogList.Add(lNewAnalog); // здесь должна быть ошибка: метод Add не существует
Z>


Z>Однако, при формировании агрегата в эти коллекции нужно же как-то добавлять элементы.

Z>То есть эти же самые коллекции, чтобы их можно было заполнить, должны иметь метод Add. Но этот метод дожен быть доступен только при формированиии коллекции.
Z>Получается, мы имеем противоречие.
Z>Как это правильно реализовать в программном коде?


Можно сделать коллекцию приватной и работать с ней внутри, либо через специальные открытые методы (а ля tovar.AddAnalog()), а наружу выставлять ее через свойство с типом IReadOnlyCollection.
Re[2]: DDD: разные коллекции внутри и снаружи агрегата
От: zelenprog  
Дата: 17.06.24 07:35
Оценка:
F>Можно сделать коллекцию приватной и работать с ней внутри, либо через специальные открытые методы (а ля tovar.AddAnalog()), а наружу выставлять ее через свойство с типом IReadOnlyCollection.

В "моем" языке программирования нету такого интерфейса.
Есть только "голые" списки, массивы. И я могу их "обернуть" в любой свой класс.
Что делать в таком случае?
Отредактировано 17.06.2024 7:36 zelenprog . Предыдущая версия .
Re[3]: DDD: разные коллекции внутри и снаружи агрегата
От: fmiracle  
Дата: 17.06.24 07:46
Оценка: +1
Здравствуйте, zelenprog, Вы писали:

F>>Можно сделать коллекцию приватной и работать с ней внутри, либо через специальные открытые методы (а ля tovar.AddAnalog()), а наружу выставлять ее через свойство с типом IReadOnlyCollection.


Z>В "моем" языке программирования нету такого интерфейса.

Z>Есть только "голые" списки, массивы. И я могу их "обернуть" в любой свой класс.
Z>Что делать в таком случае?

1. Сделать и использовать свои интерфейсы для таких коллекций. В моем-то языке тоже такого интерфейса нет, он есть в стандартной библиотеке моего языка. Можно использовать и собственные библиотеки же.
2. Прописать инструкции для команды как правильно пользоваться и не пользоваться коллекциями в агрегатах и отслеживать это на Code Review. Я серьезно — в конце концов проектирование системы нужно не ради самого проектирования, чтобы оно было идеально прекрасным, а ради того, чтобы сделать и поддерживать конечную систему для пользователей.
3. Не использовать DDD в коде.
Re[4]: DDD: разные коллекции внутри и снаружи агрегата
От: zelenprog  
Дата: 17.06.24 08:11
Оценка:
F>1. Сделать и использовать свои интерфейсы для таких коллекций.

Тогда вопрос: как реализовать такие коллекции?
Дело в том, что в моей среде один и тот же объект может быть преобразован к двум разным интерфейсам, но возможно и обратное преобразование.
То есть, возможна такая ситуация:
— создаем класс "T_MyCollection", который реализует и "IEditableCollection" и "IReadOnlyCollection"
— внутри агрегата создаем объект этого класса
— работаем внутри агрегата с этим объектом через интерфейс IEditableCollection
— через свойство "выставляем" этот объект как интерфейс IReadOnlyCollection
— клиент, получив объект, может преобразовать его в "исходный" тип T_MyCollection и изменить коллекцию

Значит, чтобы клиент не мог извне менять коллекцию мне нужно создать два отдельных класса:
— class T_MyEditableCollection : IEditableCollection
— class T_MyReadOnlyCollection : IReadOnlyCollection
Верно?

Предположим я сделал эти два класса, которые реализуют интерфейсы, и для реализации используют списки элементов.
Тогда сценарий работы с агрегатом будет такой:
— при чтении данных создаем объект класса T_MyEditableCollection (lAnalogs_MyEditableCollection = new T_MyEditableCollection) и заполняем его нужными элементами
— при создании агрегата передаем ему через конструктор объект lAnalogs_MyEditableCollection
— внутри агрегата создаем объект класса T_MyReadOnlyCollection: lAnalogs_MyReadOnlyCollection = new T_MyReadOnlyCollection
— данные из lAnalogs_MyEditableCollection передаем в объект lAnalogs_MyReadOnlyCollection
— через свойство агрегата возвращаем ссылку на lAnalogs_MyReadOnlyCollection

Вопрос: как передать данные (список элементов) из lAnalogs_MyEditableCollection в объект lAnalogs_MyReadOnlyCollection?
Перекачивать каждый элемент коллекции — неправильно — это лишнее.
Значит, эти два объекта должны каким-то образом "разделять" общий список элементов.
А как это правильно сделать?
Отредактировано 17.06.2024 8:16 zelenprog . Предыдущая версия . Еще …
Отредактировано 17.06.2024 8:14 zelenprog . Предыдущая версия .
Re[5]: DDD: разные коллекции внутри и снаружи агрегата
От: RushDevion Россия  
Дата: 17.06.24 08:40
Оценка:
Z>А как это правильно сделать?

public sealed class Tovar {
  // Дефолтный конструктор
  public Tovar(... /* тут какие-то обязательные свойства, без которых сущность "товар" не имеет смысла */ ) {}
  
  // Конструктор, который используется только при поднятии из БД.
  // Обычным потребителям класса он недоступен (internal), им будет пользоваться только код репозиториев.
  internal Tovar(..., IEnumerable<Analog> analogs) {
    _analogs = new List<Analog>(analogs);
  }

  // Внутри работаем с обычным список, наружу выставляем как только для чтения 
  private List<Analog> _analogs = new();
  public IReadOnlyCollection<Analog> Analogs { get; } => _analogs;

  // Контролируемое добавление аналогов (если нужно)
  public void AddAnalog(Analog analog) {
    // TODO: здесь бизнес-логика по добавлению аналога (e.g. проверка дубликатов и т.п.)
    _analogs.Add(analog);
  }
}
Re[6]: DDD: разные коллекции внутри и снаружи агрегата
От: zelenprog  
Дата: 17.06.24 09:31
Оценка:
RD>
RD>public sealed class Tovar {
RD>  ...
RD>  public IReadOnlyCollection<Analog> Analogs { get; } => _analogs;
RD>  ...
RD>}
RD>


Суть понятна, спасибо.
Но вот строчка, которую я оставил в коде — в "моем" ЯП программирования работать не будет.
Я про это подробно написал в моем предыдущем посте.
Используемый мной ЯП возвратит в этом случае List<Analog>.
Такой вот язык программирования: передает через параметры и возвращает только сами объекты без каких-либо преобразований типа.
Все преобразования надо делать "вручную": создать объект другого типа и "привязать" его к данным конвертируемого объекта или к самому конвертируемому объекту.

Значит, мне нужно сделать явное создание какого-либо объекта, реализующего только методы IReadOnlyCollection, и "подсунуть" в него List<Analog>.
Верно?
Отредактировано 17.06.2024 9:36 zelenprog . Предыдущая версия .
Re[7]: DDD: разные коллекции внутри и снаружи агрегата
От: RushDevion Россия  
Дата: 17.06.24 09:47
Оценка:
Z>Значит, мне нужно сделать явное создание какого-либо объекта, реализующего только методы IReadOnlyCollection, и "подсунуть" в него List<Analog>.
Z>Верно?

Мой пример был для языков, которые поддерживают концепцию интерфейсов (e.g. C#, Java/Kotlin).
И я хз, как это идеологически правильно выразить в "твоем" ЯП.
Например, clean реализация DDD на F# или GoLang будет мало похожа на C#.

Ну, вот так например, можно сделать:
public ReadOnlyCollection<Analog> Analogs { get; } => new ReadOnlyCollection<Analog>(_analogs); // Тут каждый раз создается read-only обертка для основной коллекции
Отредактировано 17.06.2024 10:23 RushDevion . Предыдущая версия .
Re[7]: DDD: разные коллекции внутри и снаружи агрегата
От: Sinclair Россия https://github.com/evilguest/
Дата: 26.06.24 16:33
Оценка: 2 (1)
Здравствуйте, zelenprog, Вы писали:
Z>Верно?
Да, вы всё верно понимаете. И сразу увидели проблему, которая возникает в "языках с интерфейсами".
Корректное решение тут ровно такое: пишете две разных обёртки, которые оборачивают один и тот же объект.
Причём это может быть как независимая обёртка:
var storage = new Item[];
this.internalCollection = new ReadWriteCollection<Item>(storage);
this.externalCollection = new ReadOnlyCollection<Item>(storage);

так и одна обёртка поверх другой:
this.internalCollection = new ReadWriteCollection<Item>(storage);
this.externalCollection = new ReadOnlyCollection<Item>(this.internalCollection );
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.