Я буду использовать Airbnb в качестве примера.
Зарегистрировав учетную запись Airbnb, вы можете стать хозяином, создав объявление. Чтобы создать объявление, пользовательский интерфейс Airbnb проведет вас через процесс создания нового объявления в несколько этапов:
Он также запомнит ваш самый дальний шаг, поэтому в следующий раз, когда вы захотите возобновить процесс, он перенаправит вас туда, где вы остановились.
Я изо всех сил пытался решить, должен ли я поместить список как совокупный корень и определить методы как доступные шаги, или рассматривать каждый шаг как свои собственные сводные корни, чтобы они были маленькими?
Листинг как совокупный корень
public sealed class Listing : AggregateRoot
{
private List<Photo> _photos;
public Host Host { get; private set; }
public PropertyAddress PropertyAddress { get; private set; }
public Geolocation Geolocation { get; private set; }
public Pricing Pricing { get; private set; }
public IReadonlyList Photos => _photos.AsReadOnly();
public ListingStep LastStep { get; private set; }
public ListingStatus Status { get; private set; }
private Listing(Host host, PropertyAddress propertyAddress)
{
this.Host = host;
this.PropertyAddress = propertyAddress;
this.LastStep = ListingStep.GeolocationAdjustment;
this.Status = ListingStatus.Draft;
_photos = new List<Photo>();
}
public static Listing Create(Host host, PropertyAddress propertyAddress)
{
// validations
// ...
return new Listing(host, propertyAddress);
}
public void AdjustLocation(Geolocation newGeolocation)
{
// validations
// ...
if (this.Status != ListingStatus.Draft || this.LastStep < ListingStep.GeolocationAdjustment)
{
throw new InvalidOperationException();
}
this.Geolocation = newGeolocation;
}
...
}
Большинство сложных классов в корне агрегата — это просто объекты-значения, а ListingStatus
— простое перечисление:
public enum ListingStatus : int
{
Draft = 1,
Published = 2,
Unlisted = 3,
Deleted = 4
}
Но ListingStep
может быть классом перечисления, в котором хранится следующий шаг, на который может перейти текущий шаг:
using Ardalis.SmartEnum;
public abstract class ListingStep : SmartEnum<ListingStep>
{
public static readonly ListingStep GeolocationAdjustment = new GeolocationAdjustmentStep();
public static readonly ListingStep Amenities = new AmenitiesStep();
...
private ListingStep(string name, int value) : base(name, value) { }
public abstract ListingStep Next();
private sealed class GeolocationAdjustmentStep : ListingStep
{
public GeolocationAdjustmentStep() :base("Geolocation Adjustment", 1) { }
public override ListingStep Next()
{
return ListingStep.Amenities;
}
}
private sealed class AmenitiesStep : ListingStep
{
public AmenitiesStep () :base("Amenities", 2) { }
public override ListingStep Next()
{
return ListingStep.Photos;
}
}
...
}
Преимущество наличия всего в корневом каталоге листинга состоит в том, что для всего будет обеспечена согласованность транзакций. И шаги определены как одна из проблем предметной области.
Недостатком является то, что совокупный корень огромен. На каждом шаге, чтобы вызвать действия листинга, вы должны загрузить корень агрегата листинга, который содержит все.
Для меня это звучит так, как если бы настройка геолокации не зависела от адреса объекта, другие шаги не зависят друг от друга. Например, название и описание объявления не имеют значения, какие фотографии вы загружаете.
Итак, я подумал, могу ли я рассматривать каждый шаг как свои собственные совокупные корни?
Каждый шаг как собственный совокупный корень
public sealed class Listing : AggregateRoot
{
public Host Host { get; private set; }
public PropertyAddress PropertyAddress { get; private set; }
private Listing(Host host, PropertyAddress propertyAddress)
{
this.Host = host;
this.PropertyAddress = propertyAddress;
}
public static Listing Create(Host host, PropertyAddress propertyAddress)
{
// Validations
// ...
return new Listing(host, propertyAddress);
}
}
public sealed class ListingGeolocation : AggregateRoot
{
public Guid ListingId { get; private set; }
public Geolocation Geolocation { get; private set; }
private ListingGeolocation(Guid listingId, Geolocation geolocation)
{
this.ListingId = listingId;
this.Geolocation = geolocation;
}
public static ListingGeolocation Create(Guid listingId, Geolocation geolocation)
{
// Validations
// ...
return new ListingGeolocation(listingId, geolocation);
}
}
...
Преимущество наличия каждого шага в качестве собственного сводного корня заключается в том, что он делает совокупные корни маленькими (в некоторых случаях я даже чувствую, что они слишком малы!), поэтому, когда они сохраняются обратно в хранилище данных, производительность должна быть выше.
Недостатком является то, что я потерял транзакционную согласованность агрегата листинга. Например, агрегат геолокации списка ссылается на список только по идентификатору. Я не знаю, должен ли я вместо этого поместить туда объект значения листинга, чтобы я мог получить больше информации, полезной в контексте, например, последний шаг, статус листинга и т. д.
Закрыть как основанный на мнении?
Я не могу найти ни одного примера в Интернете, где показано, как смоделировать этот волшебный стиль в DDD. Кроме того, большинство примеров, которые я нашел о разделении огромных совокупных корней на несколько более мелких, относятся к отношениям «один ко многим», но мой пример здесь в основном относится к отношениям «один к одному» (возможно, за исключением фотографий).
Я думаю, что мой вопрос не будет основан на мнении, потому что
- Существует только ограниченное количество способов моделирования агрегатов в DDD.
- В качестве примера я представил конкретную бизнес-модель airbnb.
- Я перечислил 2 подхода, о которых я думал.
Вы можете предложить мне, какой подход вы бы выбрали и почему, или другие подходы, отличные от двух перечисленных мною и причин.