Как использовать агрегаты DDD с TimeSeries?

Я пытаюсь реализовать более ориентированное на DDD решение для управления данными временных рядов. Следующие примеры кода и шаблоны можно найти здесь eShopOnWeb. По сути, это три сущности. Site, Signal и Sample. Site может иметь коллекцию Signals, а Signal может иметь коллекцию образцов.

    public class Site: BaseEntity, IAggregateRoot
    {
        // Collection loaded by EFCore through Repository
        private List<Signal> signals = new List<Signal>();

        // Public read only access
        public IEnumerable<Signal> Signals => this.signals.AsReadOnly();
    }
    public class Signal: BaseEntity, IAggregateRoot
    {
        // Signal has to belong to Site
        public int SiteId { get; private set; }

        // Typical EF Nav property removed
        // Signal should have no access to it's 'parent' properties
        // public Site Site { get; set;}

        private List<Sample> samples = new List<Sample>();

        public IEnumerable<Sample> Samples => this.samples.AsReadOnly();
    }
    public class Sample : BaseEntity
    {
        public int SignalId { get; private set; }

        public DateTime TimeStamp { get; set; }

        public double? Value { get; set; }
    }

В качестве первого шага, борясь с отсутствием доступных книг Эванса или Вернона (они есть в посте), я остановился на двух AggregateRoot с Site наиболее заметным. То есть Signal агрегат действительно должен быть доступен через Site.

Основная проблема, которую я обнаружил, связана с загрузкой подмножеств Samples в Signal.

В соответствии с шаблоном Specification, используемым в примерах eShopOnWeb, я могу довольно легко работать с Site агрегатом и загрузите его Signals совокупную коллекцию с вызовом SiteRepository на уровне Infrastructure:

    public sealed class SiteFilterSpecification : BaseSpecification<Site>
    {
        public SiteFilterSpecification(int id)
            : base(s => s.Id == id)
        {
            this.AddInclude(s => s.Signals);
        }
    }

Если я нахожусь в Service классе, где мне предоставили сайт и период времени, в течение которого что-то должно быть вычислено, обычно с несколькими Signals, шаблон спецификации предложит что-то вроде:

    public double GetComplexProcess(Site site, DateTime start, DateTime end)
    {
        var specification = new SiteSignalsWithSamplesSpec(site.Id, start, end);
        var signals = this.SignalRepository.List(specification);

        // signals should be loaded with the appropriate samples...
    }

Проблема, которую я здесь обнаружил, заключается в том, что в спецификации невозможно отфильтровать Samples, которые включены в Signal

    public sealed class SiteSignalsWithSamplesSpecification : BaseSpecification<Signal>
    {
        public SiteSignalsWithSamplesSpecification(int siteId, DateTime from, DateTime end)
            : base(s => s.SiteId == siteId)
        {
            // This throws exception at runtime
            this.AddInclude(s => s.Samples.Where(sa => sa.TimeStamp >= from && sa.TimeStamp <= end));
        }
    }

Вы можете использовать этот подход и загрузить все Samples, но при обработке данных временных рядов это может означать сотни тысяч сущностей, когда нам действительно нужен их сфокусированный выбор.

Чем я сейчас занимаюсь; и это не кажется особенно "чистым" - реализация версии класса Generic Repository специально для частичной загрузки Sample данных в Signal сущностях.

    public interface ISignalRepository : IAsyncRepository<Signal>
    {
        Task<IEnumerable<Signal>> GetBySiteIdWithSamplesAsync(int siteId, DateTime from, DateTime to);
    }
    public class SignalRepository : EfRepository<Signal>, ISignalRepository
    {
        public SignalRepository(ForecastingContext dbContext) : base(dbContext)
        {
        }

        public async Task<IEnumerable<Signal>> GetBySiteIdWithSamplesAsync(int siteId, DateTime from, DateTime to)
        {
            var signals = await this.dbContext.Signals.Where(s => s.SiteId == siteId).ToListAsync();

            foreach (var signal in signals)
            {
                this.dbContext.Entry(signal)
                    .Collection(s => s.Samples)
                    .Query()
                    .Where(s => s.TimeStamp >= from && s.TimeStamp <= to)
                    .Load();
            }

            return signals;
        }
    }

Вероятно, это всего лишь начальная неуверенность, возникающая при развитии нового паттерна, но это почему-то кажется неправильным.

Верно ли, что я использую два агрегата?


person Ross Halliday    schedule 19.02.2019    source источник


Ответы (1)


Более сложный вопрос - как загрузить образцы сущностей.

Я обнаружил, что мне нужно быть осторожным, различая два разных типа информации; информация, где моя модель является авторитетной, и справочные данные.

Возможно, вы захотите просмотреть Внешние данные по сравнению с данными изнутри.

Сигналы от датчиков в реальном мире не относятся к нашей модели. Мы просто храним его копии здесь, потому что это более рентабельно, чем пытаться хранить все это здесь. Таким образом, нам не нужны «агрегаты», когда наша задача - сбор справочных данных.

Тем не менее, мы собираем данные, потому что мы, конечно, хотим сделать с ними что-то - поэтому у нас может быть модель предметной области, которая объединяет разделы наших захваченных данных вместе для выполнения интересных вычислений. Но это - в моем опыте - является параллельным поведением; процесс агрегирования данных не должен мешать нам собирать больше.

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

person VoiceOfUnreason    schedule 19.02.2019
comment
Для этого понадобится серьезная чашка кофе; Я прочитаю его завтра и посмотрю, куда мы пойдем. - person Ross Halliday; 19.02.2019
comment
Хорошая бумага; интересно наблюдать, как концепции развивались с годами и как на них основываются микросервисы и доменные события. Итак ... комментарий. Да, я согласен, это то, что мы делаем. Множество внешних сервисов, передающих данные в это приложение (подталкивая, скорее, подталкивая, но это более поздний рефакторинг), управляющий этим код работает на уровне SampleRepository; что приемлемо, так как его EFCore и выполнение массовых операций для производительности. Я бы хотел, чтобы ApplicationServices работал больше с DDD, что подразумевает семантику Site.Signal.Samples. Я обновил свой вопрос :-) - person Ross Halliday; 19.02.2019