DDD实现2

DDD实现2

写在开头

上节我们已经讲过了聚合根实体等,我们将继续这些话题

实现

仓储(Repository)

首先是仓储概念,我吗上期说到了,想要访问聚合内部其他实体,只能通过聚合根来访问,怎么控制呢,封装仓储接口,只有聚合根才能使用

public interface IRepository<T> : IRepositoryBase<T> where T : class, IAggregateRoot
{
}

注意:查询时,仓储不能返回IQueryable,只能返回具体的实体或者列表,我们需要隐藏数据访问的细节!

规范(Specification)

既然在仓储中无法使用select,where等,所以我吗需要引入规范模式

规范模式将查询特定的逻辑从应用程序中当前存在的其他位置提取出来

public sealed class PlanOverNextDateSpec:Specification<MaintenancePlan>
{
  public PlanOverNextDateSpec()
  {
    Query
      .Where(a => a.NextDateTime <= DateTime.UtcNow);
  }
}
//调用
 var plans=await planRepository.ListAsync(new PlanOverNextDateSpec(),cancellationToken);

如上,我们创建了一个Specification,并传入了仓储中

我使用ardalis的specification库

充血模型(Rich Model)

我在此进行补充,这是一个跟贫血模型(Anemic Model)相反的概念。

我对此的理解是,将属于实体/值对象的业务逻辑还给他

比如,在维护周期中:我们在内部定义了一个GetNextDate来获取下次维护时间

public class MaintenanceCycle(MaintenanceCycleType cycleType, int intervalTime):ValueObject
{
  public MaintenanceCycleType CycleType { get; private set; }=Guard.Against.Null(cycleType);
  public int IntervalTime { get; private set; } = Guard.Against.NegativeOrZero(intervalTime);
  public int DayOfPeriod { get; private set; } = 0;
  public void SetDayOfPeriod(int dayOfPeriod)
  {
    DayOfPeriod = Guard.Against.NegativeOrZero(dayOfPeriod);
  }
  
  public virtual DateTime GetNextDate(DateTime startDate)
  {

    if (CycleType == MaintenanceCycleType.Day)
    {
      return startDate.AddDays(IntervalTime);
    }
    if (CycleType == MaintenanceCycleType.Month)
    {
      var nextDate = new DateTime(startDate.Year,  startDate.Month, DayOfPeriod).AddMonths(1);
      return startDate.Day <= DayOfPeriod ?  nextDate: nextDate.AddMonths(1);
    }
    if(CycleType == MaintenanceCycleType.Week)
    {
      var dayOfWeek=  (int)startDate.DayOfWeek;
      var diff = DayOfPeriod -dayOfWeek;
      return startDate.AddDays(IntervalTime * 7).AddDays(diff < 0 ? dayOfWeek+DayOfPeriod : diff);
    }
    return DateTime.UtcNow;
  }
  protected override IEnumerable<object> GetEqualityComponents()
  {
    yield return CycleType;
    yield return IntervalTime;
    yield return DayOfPeriod;
  }
}

领域服务(Domain Service)

当一段业务逻辑无法放到 实体/值对象中时,则需要领域服务

以下代码处理标记为已完成维护任务对应的维护计划,更新维护计划的上次维护时间

public class MaintenanceTaskService(IMediator mediator,IRepository<MaintenancePlan> planRepo):IMaintenanceTaskService
{
  public async Task ConfirmTask(List<MaintenanceTask> tasks)
  {
    var spec = new GetPlanByIdsSpec(tasks.Select(a=>a.MaintenancePlanId).ToList());
    var plans = await planRepo.ListAsync(spec);
    plans.ForEach(p=>p.UpdateLastDateTime());
    await planRepo.SaveChangesAsync();
    // 发布事件
    await mediator.Publish(new TaskConfirmedEvent(tasks));
  }
}

应用服务(Application Service)

该层不包含业务规则,只用于协调多个聚合间的调用.

参考以下代码,用于处理维护确认,我使用的是CQRS模式,我们

1.调用仓储获得了聚合根

2.调用了聚合跟内部业务逻辑

3.调用了领域服务

public class ConfirmTaskHandler(IMaintenanceTaskService taskService,IRepository<MaintenanceTask> taskRepo): ICommandHandler<ConfirmTaskCommand,Result>
{
  public async Task<Result> Handle(ConfirmTaskCommand request, CancellationToken cancellationToken)
  {
    var taskIds=request.Tasks.Select(a=>a.TaskId).ToList();
    var taskDict = request.Tasks.ToDictionary(a => a.TaskId, a => a);
    var tasksInDb = await taskRepo.ListAsync(new GetTasksByIdsSpec(taskIds),cancellationToken);
    foreach (var task in tasksInDb)
    {
      if (!taskDict.TryGetValue(task.Id, out var value))
        continue;

      if(value.IsPassed)task.MarkConfirmed();
      else task.Reject(value.AuditOpinion);
    }

    await taskRepo.SaveChangesAsync(cancellationToken);
    await taskService.ConfirmTask(tasksInDb);
    return Result.Success();
  }
}

领域事件(Domain Event)

用于表示领域内发生的一件事,比如维护任务已经确认事件,通常

1.由领域服务内触发

2.在实体内部注册,在ef core SaveChanges中进行触发

public class TaskConfirmedHandler(ILogger<TaskConfirmedHandler> logger): INotificationHandler<TaskConfirmedEvent>
{
  public async Task Handle(TaskConfirmedEvent notification, CancellationToken cancellationToken)
  {
    //TODO:发送任务完成通知
    logger.LogWarning("以下维护任务已完成:{taskIds}",notification.Tasks);
    
    
    //TODO:生成任务记录
    await Task.CompletedTask;
  }
}

集成事件(Integration Event)

通常是跨限界上下文时使用,比如设备台账属于设备基础信息上下文,维护计划属于设备维护上下文,当设备设置为停机状态,则设备基础信息上下文发布集成事件,设备维护上下文订阅,并改变关联的维护计划的状态

此处不做代码示例,我们可以使用MassTransit来实现

写在最后

我们已经涵盖了大部分的DDD概念,我会在后续创建一个示例github仓库,作为采用DDD+CRQS 的一个简单示例,将基于之前的设备维护事件建模图。如有错误,欢迎指正!

posted @ 2025-05-24 10:46  ssz0312  阅读(44)  评论(0)    收藏  举报