【问题标题】:Refactoring to Domain driven design重构为领域驱动设计
【发布时间】:2015-01-26 21:51:11
【问题描述】:

我有一个场景正在尝试重构为 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>{}

域和域服务通过应用程序服务公开。 应用服务中的代码如下:

//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));
    }
}

问题:

  1. 应将 BatchStatus(即 Running、Posted)分配到哪里?
  2. 是否应将 MarkBatchAsPosted 方法定义为批处理实体中的方法?
  3. 如何才能最好地重新设计这方面的领域驱动设计?

【问题讨论】:

  • 刚刚看到那个老问题。毕竟我的回答有帮助吗?
  • @plalx 它确实把我引向了正确的方向

标签: c# oop domain-driven-design onion-architecture


【解决方案1】:

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

语句如

"创建 Batch 并添加 BatchEntries 后,将发送 SMS 到 批次中的个人和批次的状态从 跑到张贴”

对我来说意义不大。一个批次真的可以是没有任何条目的批次吗?如果没有,为什么添加条目时批处理会自动启动?

无论如何,我没有冒险回答您的 3 个问题,但是您似乎违反了一些准则,理解它们将使您能够提出自己的答案:

  • 您的域受到anemia 的影响。

  • 非根聚合不应有自己的存储库,因为它们只能通过根访问。聚合根的子节点只能通过它们的根进行修改(告诉不要问)。 如果EntryRepository 不是根,则不应有BatchEntryRepository

  • 聚合根是一个事务边界,在同一个事务中只能修改一个。此外,聚合根应尽可能小,因此您只保留在集群内强制执行不变量所需的部分。 在您的情况下,添加/删除批处理条目似乎会影响Batch 的状态,因此在Batch 下收集BatchEntry 是有意义的,并且可以保护不变量事务。

    注意:如果Batch 有很多争用,例如多人在同一个Batch 实例上工作,添加和删除BatchEntry 实例,那么您可能必须将BatchEntry 设为自己的聚合根并使用最终一致性将系统带入一致状态。

  • 通常应该使用始终有效的方法来设计域对象,这意味着它们永远不会处于无效状态。 UI 通常应该负责验证用户输入以避免发送不正确的命令,但域可能只会让你失望。 因此,validator.IsValid&lt;Batch&gt;(batch) 几乎没有任何意义,除非它验证了 Batch 无法自行执行的内容。

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

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

希望我的回答能帮助您完善模型,但我强烈建议您阅读EvansVernon... 或两者兼而有之;)

【讨论】:

  • 这是一本很好的读物。我有大量信息可以帮助我进行重构。谢谢
  • @mkaris 很高兴为您提供帮助。如果您有更具体的问题,请告诉我。
  • 我想对 做出反应对我来说意义不大。批次真的可以是没有任何条目的批次吗?如果没有,为什么添加条目时批次会自动启动? 业务规则规定,创建的批次没有行项目,但至少应具有描述、批次号和总值和状态(待定 - 从运行后认真思考)。条目将在稍后阶段添加。 batchtotal 值将用于检查批次是否已完全分配。保存后(更新条目)状态将更改为已发布。
  • @mkaris 现在更有意义了 ;) 因此,当达到阈值时,批处理会自动启动。
  • 就是这样。但我希望我能把它作为一个过程。从逻辑上讲,没有条目的批次是没有意义的。
猜你喜欢
  • 2011-10-06
  • 2014-09-12
  • 2015-09-21
  • 2016-09-29
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
  • 2011-02-04
相关资源
最近更新 更多