重构到域驱动设计

alg*_*hiz 0 c# oop domain-driven-design onion-architecture

我有一个场景我试图重构DDD.我有一个Batch,它是一个聚合和BatchEntries列表.创建批处理并添加BatchEntries后,将向批处理中的个人发送SMS,并且批处理的状态将从运行更改为已发布.

关于如何使设计更好的任何想法?该域有两个聚合Batch和BatchEntry,Batch是聚合根.

代码看起来像这样

public class Batch : EntityBase, IValidatableObject
{
    public int BatchNumber { get; set; }
    public string Description { get; set; }
    public decimal TotalValue { get; set; }
    public bool SMSAlert { get; set; }
    public int Status { get; set; }

    private HashSet<BatchEntry> _batchEntries;
    public virtual ICollection<BatchEntry> BatchEntries
    {
        get{
            if (_batchEntries == null){
                _batchEntries = new HashSet<BatchEntry>();
            }
            return _batchEntries;
        }
        private set {
            _batchEntries = new HashSet<BatchEntry>(value);
        }
    }

    public static Batch Create(string description, decimal totalValue, bool smsAlert)
    {
        var batch = new Batch();
        batch.GenerateNewIdentity();
        batch.Description = description;
        batch.TotalValue = totalValue;
        batch.SMSAlert = smsAlert;
        return batch;
    }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // 
    }
}

public interface IBatchRepository : IRepository<Batch>
{
    int NextBatchNumber();
}

public class BatchEntry : EntityBase, IValidatableObject
{
    public Guid BatchId { get; set; }
    public virtual Batch Batch { get; private set; }
    public decimal Amount { get; set; }
    public Guid CustomerAccountId { get; set; }
    public virtual CustomerAccount CustomerAccount { get; private set; }

    public static BatchEntry Create(Guid batchId, Guid customerAccountId, decimal amount)
    {
        var batchEntry = new BatchEntry();
        batchEntry.GenerateNewIdentity();
        batchEntry.BatchId = batchId;
        batchEntry.CustomerAccountId = customerAccountId;
        batchEntry.Amount = amount;
        return batchEntry;
    }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        //
    }
}

public interface IBatchEntryRepository : IRepository<BatchEntry>{}
Run Code Online (Sandbox Code Playgroud)

域和域服务通过Application Services公开.申请服务中的守则如下:

//Application Services Code

public class BatchApplicationService : IBatchApplicationService
{
    private readonly IBatchRepository _batchRepository;
    private readonly IBatchEntryRepository _batchEntryRepository;

    public BatchAppService(IBatchRepository batchRepository, IBatchEntryRepository batchEntryRepository)
    {
        if (batchRepository == null) throw new ArgumentNullException("batchRepository");

        if (batchEntryRepository == null) throw new ArgumentNullException("batchEntryRepository");

        _batchRepository = batchRepository;
        _batchEntryRepository = batchEntryRepository;
    }

    public BatchDTO AddNewBatch(BatchDto batchDto)
    {
        if (batchDto != null)
        {
            var batch = Batch.Create(batchDto.Description, batchDto.TotalValue, batchDto.SMSAlert);
            batch.BatchNumber = _batchRepository.NextBatchNumber();
            batch.Status = (int)BatchStatus.Running;
            SaveBatch(batch);
            return batch.Map<BatchDto>();
        }
        else
        {
            //
        }
    }

    public bool UpdateBatch(BatchDto batchDto)
    {
        if (batchDto == null || batchDto.Id == Guid.Empty)
        {
            //
        }

        var persisted = _batchRepository.Get(batchDto.Id);
        if (persisted != null)
        {
            var result = false;
            var current = Batch.Create(batchDto.Description, batchDto.TotalValue, batchDto.SMSAlert);
            current.ChangeCurrentIdentity(persisted.Id);
            current.BatchNumber = persisted.BatchNumber;
            current.Status = persisted.Status;

            _batchRepository.Merge(persisted, current);
            _batchRepository.UnitOfWork.Commit();

            if (persisted.BatchEntries.Count != 0){
                persisted.BatchEntries.ToList().ForEach(x => _batchEntryRepository.Remove(x));
                _batchEntryRepository.UnitOfWork.Commit();
            }

            if (batchDto.BatchEntries != null && batchDto.BatchEntries.Any())
            {
                List<BatchEntry> batchEntries = new List<BatchEntry>();
                int counter = default(int);
                batchDTO.BatchEntries.ToList().ForEach(x =>
                {
                    var batchEntry = BatchEntry.Create(persisted.Id, x.CustomerAccountId, x.Amount);
                    batchEntries.Add(batchEntry);
                });
            }
            else result = true;
            return result;
        }
        else
        {
            //
        }
    }

    public bool MarkBatchAsPosted(BatchDto batchDto, int authStatus)
    {
        var result = false;
        if (batchDto == null || batchDto.Id == Guid.Empty)
        {
            //
        }

        var persisted = _batchRepository.Get(batchDto.Id);
        if (persisted != null)
        {
            var current = Batch.Create(batchDto.Description, batchDto.TotalValue, batchDto.SMSAlert);
            current.ChangeCurrentIdentity(persisted.Id);
            current.BatchNumber = persisted.BatchNumber;
            current.Status = authStatus;
            _batchRepository.Merge(persisted, current);
            _batchRepository.UnitOfWork.Commit();
            result = true;
        }
        else
        {
            //
        }
        return result;
    }

    private void SaveBatch(Batch batch)
    {
        var validator = EntityValidatorFactory.CreateValidator();
        if (validator.IsValid<Batch>(batch))
        {
            _batchRepository.Add(batch);
            _batchRepository.UnitOfWork.Commit();
        }
        else throw new ApplicationValidationErrorsException(validator.GetInvalidMessages(batch));
    }
}
Run Code Online (Sandbox Code Playgroud)

问题:

  1. 应该在哪里分配BatchStatus即Running,Posted?
  2. 是否应将MarkBatchAsPosted方法定义为批处理实体中的方法?
  3. 如何最好地重新设计域驱动设计?

pla*_*alx 7

虽然看起来很简单,但我不确定我是否真的了解您的域名.

声明如

"在创建批次并添加BatchEntries后,将向批次中的个人发送SMS,并且批次的状态从运行更改为已发布"

对我来说没什么意义.批次真的可以是没有任何条目的批次吗?如果没有,为什么批次会在添加条目时自动启动?

无论如何,我没有冒回答你的3个问题的风险,但是你似乎违反了一些指导原则并理解它们将允许你提出自己的答案:

  • 您的域名患有贫血症.

  • 非根聚合不应该有自己的存储库,因为它们只能通过root访问.聚合根的子项只能通过其根修改(Tell Do not Ask).你不应该有一个BatchEntryRepositoryif EntryRepository不是root.

  • 聚合根是一个事务边界,只应在同一事务中修改一个.此外,聚合根应尽可能小,因此您只需保留在群集中强制实施不变量所需的部分.在您的情况下,添加/删除批处理条目似乎会影响其Batch状态,因此拥有一个BatchEntryunder 的集合是Batch有意义的,并允许保护不变量transactionnaly.

    注意:如果a上存在大量争用Batch,例如多个人在同一个Batch实例上工作,添加和删除BatchEntry实例,那么您可能必须使其BatchEntry自己的聚合根并使用实际一致性来使系统处于一致状态.

  • 域对象通常应使用始终有效的方法设计,这意味着它们永远不会处于无效状态.用户界面通常应该注意验证用户输入,以避免发送错误的命令,但域名可以只是扔给你.因此,validator.IsValid<Batch>(batch)除非它正在验证Batch无法自行执行的某些内容,否则几乎没有意义.

  • 域逻辑不应泄漏应用程序服务,并且通常应尽可能封装在实体中(否则为域服务).您目前正在应用程序服务中执行许多业务逻辑,例如 if (persisted.BatchEntries.Count != 0){ ... }

  • DDD不是CRUD.在CRUD中使用战术DDD模式不是必要的错误,但肯定不是DDD.DDD是关于无处不在的语言和域的建模.当你看到名为Update...或大量的方法时getter/setters,通常意味着你做错了.DDD最适用于基于任务的UI,它允许一次关注一个业务操作.您的UpdateBatch方法做得太多,应该分成更有意义和更细粒度的业务操作.

希望我的回答可以帮助你改进你的模型,但我强烈建议你阅读埃文斯弗农 ...或两者;)