我正在尝试实现一个更面向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; }
}
作为第一遍,在没有可用的埃文斯或弗农书籍的情况下挣扎(它们在帖子中),我决定有两个 AggregateRoots,Site
更突出的一个。这是一个Signal
聚合,实际上应该通过Site
访问。
我发现的主要问题是将Samples
子集加载到Signal
。
根据 eShopOnWeb 示例中使用Specification
模式,我可以相当轻松地使用Site
聚合,并通过对Infrastructure
层中的SiteRepository
调用Signals
加载它的聚合集合:
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...
}
我在这里发现的问题是,在规范中无法过滤Signal
中包含的Samples
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
但在处理时间序列数据时,这可能意味着数十万个实体,而我们真正需要的是集中选择它们。
我目前正在做的;这感觉不是特别"干净"的是实现一个版本的通用存储库类,专门用于在Signal
实体上部分加载Sample
数据。
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;
}
}
这可能只是以新模式发展带来的初始不确定性,但这在某种程度上感觉不对。
我使用两个聚合是否正确?
更困难的问题是如何加载示例实体
我发现我需要小心区分两种不同类型的信息:我的模型是权威的信息,以及参考数据。
您可能需要查看外部数据与内部数据。
来自现实世界中传感器的信号不属于我们的模型。 我们只是在这里存储它的副本,因为这比尝试将其全部存储在那里更具成本效益。 因此,当手头的任务是参考数据捕获时,我们不需要"聚合"。
也就是说,我们捕获数据是因为我们想用它做点什么,当然 - 所以我们可能有一个域模型,将捕获的数据的分区聚合在一起以执行有趣的计算。 但是,在我所经历的经历中,这是一种并行的行为;聚合数据的过程不应该阻止我们收集更多数据。
相反,它通常看起来像是从外部世界传入的数据流,并在内部过程中进行簿记,并带有更新的参考,以跟踪它在已到达信号历史记录中的位置。
同样值得记住的是,DDD不仅仅是战术模式。当我们有复杂的写入模型时,聚合模型是有意义的,你想要保护事务边界。 但是,正如已经提到的,时间序列是事件的集合,除了执行类型安全控制(布尔值与数字)之外,您没有业务不变性来保护实体的状态。事实上,我们可以说你只有一个值集合,但没有实体存在,除非你想使用时间戳作为给定模式的标识符。但这主要是心理体操,而不是问题空间中的中小企业真正认为是实体标识符的东西。该实体没有进一步的突变,只是一次发生过的时间读取操作。
时间序列是仅追加模型。试图将聚合和存储库模式强制到一个与 Evans 和 Vernon 在写书时的想法没有内在关系的模型上没有多大价值。
不过,仍然存在的是战略方面、边界上下文、无处不在的语言和整合模式。
相反,我建议更多地关注这些问题,而不是试图强制代码遵循不太适合的模型,从而造成人为阻抗不匹配。