重构为领域驱动设计
本文关键字:重构 | 更新日期: 2023-09-27 18:32:16
我有一个场景正在尝试重构为 DDD。我有一个批处理,它是一个聚合和批处理条目列表。创建批处理并添加批处理条目后,将向批处理中的个人发送一条短信,并且批处理的状态将从正在运行更改为已发布。
关于如何使设计更好的任何想法?该域有两个聚合 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));
}
}
问题:
- 批处理状态(即正在运行、已发布)应分配到何处?
- 是否应将 MarkBatchAsPosted 方法定义为批处理实体中的 mehod?
- 如何最好地为领域驱动设计重新设计?
虽然看起来很简单,但我不确定我是否真的了解您的域。
诸如
"创建批处理并添加批处理条目后,将发送一条短信到 批次中的个人和批次的状态从 运行到发布"
对我来说意义不大。批次真的可以成为没有任何条目的批次吗?如果不是,为什么在添加条目时批处理会自动启动?
无论如何,我没有冒险回答你的 3 个问题,但有一些你似乎违反了的指导方针,理解它们将让你提出自己的答案:
-
您的域名患有贫血症。
-
非根聚合不应有自己的存储库,因为它们只能通过根访问。聚合根的子项只能通过其根进行修改(告诉不要问)。如果
EntryRepository
不是根,则不应有BatchEntryRepository
。 -
聚合根是一个事务边界,在同一事务中只应修改一个。此外,聚合根应尽可能小,因此您只保留在集群中强制实施不变所需的片段。在您的情况下,添加/删除批处理条目似乎会影响
Batch
的状态,因此在Batch
下收集BatchEntry
是有意义的,并且允许以事务方式保护不变性。注意:如果
Batch
上存在大量争用,例如,多个人在同一个Batch
实例上工作,添加和删除BatchEntry
实例,那么您可能必须BatchEntry
它自己的聚合根,并使用现代一致性使系统保持一致状态。 -
域对象通常应使用始终有效的方法进行设计,这意味着它们永远不会处于无效状态。UI 通常应负责验证用户输入以避免发送不正确的命令,但域可能会扔给你。因此,
validator.IsValid<Batch>(batch)
几乎没有意义,除非它验证了Batch
不可能自行强制执行的内容。 -
域逻辑不应在应用程序服务中泄漏,并且通常应尽可能封装在实体中(否则应封装在实体中。您当前正在应用程序服务中执行大量业务逻辑,例如
if (persisted.BatchEntries.Count != 0){ ... }
-
DDD 不是 CRUD。在 CRUD 中使用战术 DDD 模式不一定是错误的,但它肯定不是 DDD。DDD 是关于无处不在的语言和域建模的。当您看到名为
Update...
或大量getter/setters
的方法时,这通常意味着您做错了。DDD 最适合基于任务的 UI,它允许一次专注于一个业务操作。您的UpdateBatch
方法做得太多了,应该将其划分为更有意义和更精细的业务运营。
希望我的回答能帮助你完善你的模型,但我强烈建议你阅读埃文斯或弗农......或两者兼而有之;)