C#. Почему в Концепции DDD описывают работу с абстрактным репозиторием только в типах AgregationRoot?

142
29 ноября 2019, 03:30

Здравствуйте начитался про DDD и одно из ЖЕСТКИХ требований следования этой концепции это работа с репозиторием только в типах "Корень Агрегации"

 public abstract class Repository<T> where T : AggregateRoot
 {
   public T GetById(long id)
   {
   }      
   public void Save( T aggregateRoot)
   {
   }
 }

Есть модель предметной области (суб домен) справочник для оценочных компаний. Бизнес логики внутри домена мало, больше нужна система хранения с CRUD операциями, но тем не менее полноценный домен выделить можно.
Каждая компания содержит список Домов для оценки, у каждого дома есть набор свойств и бизнес логики.

P.S. В примере, сознательно, указываю простую доменную модель (публичные get, set для свойств).

базовые классы:
DomainEntity - содержит Id, объект мутабелен
DomainValueObject - НЕ содержит Id объект имутабелен.
public class Company : AggregateRoot
{    
    public string Name { get; set; } 
    public List<House> Houses { get; set; }    
}
public class House : DomainValueObject<House>
{     
    public string City { get; set; }                // Город
    public string District { get; set; }            // Район
    public string Street { get; set; }              // Улица
    public string Number { get; set; }              // Дом
    public int? Year { get; set; }                  // Год постройки
    public string MetroStation { get; set; }        // Ближайшая станция метро
    public string Geo { get; set; }                 // гео координаты
    public WallMaterial WallMaterial { get; set; }
    //бизнес логика......
}
public class WallMaterial : DomainValueObject<WallMaterial>
{
  public string Name { get; }  
  //бизнес логика......
}

В моем примере Company - это корень агрегации

  1. Чтение всех домов этой компании. мне придется тянуть всю компанию.

     var company= companyRepository.GetById(1);//Тут все зависимости (полный объект Домена)
     var houses= company.Houses;
    

На это у DDD есть ответ, делать Dto для возвращаемых объектов (в частных случаях).

var housesDto= companyRepository.GetAllHousesDto(); 

Возвращается не House а его подделка, для соблюдения концепции - нельзя работать с агрегатом по частям, а только через Корень агрегации. housesDto не является частью агрегата, это его подделка. Тут, понятно и не сильно больно следовать этой концепции.

  1. Добавить новый дом.

Т.е. добавляя новый, дом мы взаимодействуем через корень агрегации: Я так понимаю предполагается такое использование репозитория

  var company= companyRepository.GetById(1);
  company.Houses.Add(newHouse); 
  companyRepository.Save(company);

Т.е. чтобы мне добавить дом, мне нужно вытянуть из БД компанию, со всеми зависимостями, добавить дом и потом сохранить. Но ведь это не оптимально? зачем мне лишняя операция чтения?

по классике (если использовать EFcore в качестве технологии хранения) делаем так.

public async Task<bool> AddHouseInCompanyAsync(long companyId, House house)
{
    var efHouse = Mapper.Map<EfHouse>(house);
    efHouse.EfCompanyId = companyId;
    var res= await _context.Houses.AddAsync(efHouse);
    return res.State == EntityState.Added;
}

Но это уже нарушение DDD, в методе репозитория используется часть агрегата (House).

Как быть? изменять объект всегда через company (т.е. через корень агрегации)? Или для моей задачи не подходить DDD и лучше использовать CRUD обертки?

Answer 1

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

Агрегат это составной объект. Например, в интернет-магазине агрегатом может быть сущность заказ, в которой есть отдельные товарные позиции, которые без заказа бесполезны. Косвенно вложенность товарных позиций подтверждают сценарии использования и соответствующие им экранные формы. Есть форма редактирования заказа в которой представлены все товарные позиции, но нет формы редактирования отдельной товарной позиции.

Если в вашей программы есть формы редактирования отдельного дома без всякой связи с компанией, значит, дом это выделенный корень агрегации.

Агрегаты могут ссылаться друг на друга, но только через идентификаторы. Содержать друг друга они не могут.

DTO из репозиториев возвращать не надо. Обычно, DTO формируются где-то вне предметной области для связи с другими слоями приложения. В самой предметной области у вас только сущности и объекты-значения, объединённые в агрегаты.

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

Велика ли потеря скорости? Нет. Есть такой паттерн, Remote Façade, описанный Фаулером. Основанием для его использования является особенность загрузки данных с удалённых компьютеров, а именно наличие так называемой задержки. При установке соединения с сервером мы тратим небольшое время, а после того, как соединение установлено, можем получать данные на большой скорости. Поэтому полезно читать чуть больше данных, чем нам может быть нужно, это будет быстрее, чем прочитать два или три раза ровно столько данных, сколько нам нужно.

Кроме того, если мы захотим перейти с реляционного хранения на документное, интерфейсы репозиториев не изменятся, слой предметной области вообще не придётся менять. Поэтому не бойтесь лишних чтений.

В EF для подгрузки связанных данных используется метод Include. Предположим, что всё-таки дома это часть компаний. Тогда при загрузке компаний нужно загрузить связанные дома:

public class CompanyRepository
{
    private readonly DbContextFactory dbContextFactory;
    private readonly DataMapper dataMapper;
    . . .
    public Company GetById(long id)
    {
        using (var dbContext = dbContextFactory.Create())
        {
            var companyData = dbContext.Companies
                                       .Include(x => x.Houses)
                                       .SingleOrDefault(x => x.Id == id);
            var company = dataMapper.FromData(companyData);
            return company;
        }
    }
}

Благодаря методу Include из базы будет загружена не только компания, но и дома, которые к ней приписаны. В этом коде видно, что мы загружаем DTO и отдаём их в объект DataMapper, который строит из них агрегат целиком, включая сущности House внутри Company.

Answer 2

Вы неправильно поняли идею с корнем агрегации и остальными сущностями.

Если сущность может существовать без корня - она сама корень.

Если дома могут существовать без компании - они тоже корни.

Пример корня на вашей предметке:

Этажи дома - должны принадлежать корню дом. Жители квартир могут быть как дочерними сущностями (если важен только факт их наличия например), так и корнями, если вы с ними например работаете как то сложнее.

Дочернюю сущность можно делать аналогично DTO - буквально сам факт наличия дочерней и в ней ссылка на другой корень. Т.е. компания (root) - оцениваемые дома (child), где структура типа child содержит буквально ссылку на дом и возможно id для хранения в базе. Потом в такой child можно добавлять примечания, какие то оценки мб, что угодно. К дому они при этом явно не относятся.

Условно:

class Company
{
  List<CompanyHouse> Houses
}
class CompanyHouse
{
  House
  Note
}
class House
{ ... }
READ ALSO
Вложить несколько элементов из БД в DisplayMember, listbox

Вложить несколько элементов из БД в DisplayMember, listbox

Есть база данный, из неё в listbox, в поле DisplayMember, вывожу из таблицы Agent, значения столбца FirstNameКак можно вывести через пробел значения столбцов...

124
Изменить настройки DataSet

Изменить настройки DataSet

Есть у меня программка на C#, которая тянет данные и пишет в MS SQLДля этого она использует DataSet

130
Работа с двумя SerialPort C# .NET

Работа с двумя SerialPort C# .NET

Задача заключается в следующемНеобходимо работать с двумя COM портами одновременно

111
Unity крашится при открытии документа Word

Unity крашится при открытии документа Word

Столкнулся с проблемойПри программном открытии документа Word с шаблоном Unity крашится

116