65.9K
CodeProject 正在变化。 阅读更多。
Home

领域驱动设计:我的 5 大最佳实践

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2016 年 9 月 24 日

CPOL

5分钟阅读

viewsIcon

52743

领域驱动设计 - 我的 5 大最佳实践

引言

大约 3 年前,我写了 《应用领域驱动设计系列》,并创建了一个 “领域驱动设计示例” Github 仓库

我注意到,我们在学习 DDD 的过程中都会犯同样的错误。在这篇文章中,我将尝试解决一些我见过或犯过的最常见的错误。这个最佳实践列表是有限的,请同时查看 Daniel Whittaker 的 "10 个领域驱动设计错误"。

1. 始终从需求与建模开始

我记得第一次发现 Eric Evans 的那本蓝皮书时。我读了它,非常喜欢。然而,我最兴奋的是抛弃贫血模型,引入聚合根、领域服务等。我错过了领域驱动设计的要点。关键在于你应该沉浸在业务需求中,多问业务问题,并与业务分析师一起在白板上建模。相反,我最终建模了我自己的、非业务导向的技术现实。这个模型无法长期维护,不得不重写。始终从需求和建模开始来避免这种情况

2. 尽可能保持聚合根扁平化

当我刚开始使用 DDD 时,我对深度对象图嵌套的想法非常兴奋。想象一下,你有一个对象 Customercustomer 对象可以访问 CartPurchasesCredit Cards 等。Purchases 可以访问购买的 items,依此类推。这很令人兴奋,因为我可以使用 Specification pattern 来遍历我的模型。这很令人兴奋,因为它使我能够编写复杂的领域查询,并将查询输出投影到非常复杂的 UI 上。不幸的是,很快,这使得单元测试非常困难,也使得应用程序难以维护。

以下是一个深度对象图的例子

public class Customer : IAggregateRoot
{
private List<Purchase> purchases = new List<Purchase>();
private List<CreditCard> creditCards = new List<CreditCard>();

public virtual Guid Id { get; protected set; }
public virtual string FirstName { get; protected set; }
public virtual string LastName { get; protected set; }
public virtual string Email { get; protected set; }
public virtual string Password { get; protected set; }
public virtual DateTime Created { get; protected set; }
public virtual bool Active { get; protected set; }
public virtual decimal Balance { get; protected set; }
public virtual Country Country { get; protected set; }

public virtual ReadOnlyCollection<Purchase> Purchases 
{ get { return this.purchases.AsReadOnly(); } }
public virtual ReadOnlyCollection<CreditCard> CreditCards 
{ get { return this.creditCards.AsReadOnly(); } }

public virtual Cart Cart { get; protected set; }

public virtual void ChangeEmail(string email)
{
    if(this.Email != email)
    {
        this.Email = email;
        DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail() { Customer = this });
    }
}

public static Customer Create(string firstname, string lastname, string email, Country country)
{
    return Create(Guid.NewGuid(), firstname, lastname, email, country); ;
}

public static Customer Create(Guid id, string firstname, string lastname, string email, Country country)
{
    if (string.IsNullOrEmpty(firstname))
        throw new ArgumentNullException("firstname");

    if (string.IsNullOrEmpty(lastname))
        throw new ArgumentNullException("lastname");

    if (string.IsNullOrEmpty(email))
        throw new ArgumentNullException("email");

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

    Customer customer = new Customer()
    {
        Id = id,
        FirstName = firstname,
        LastName = lastname,
        Email = email,
        Active = true,
        Created = DateTime.Today,
        Country = country
    };

    DomainEvents.Raise<CustomerCreated>(new CustomerCreated() { Customer = customer });

    customer.Cart = Cart.Create(customer);

    return customer;
}

public virtual ReadOnlyCollection<CreditCard> GetCreditCardsAvailble()
{
    return this.creditCards.FindAll
        (new CreditCardAvailableSpec(DateTime.Today).IsSatisfiedBy).AsReadOnly();
} 

public Nullable<PaymentIssues> IsPayReady()
{
    if (this.Balance < 0)
        return PaymentIssues.UnpaidBalance;

    if (this.GetCreditCardsAvailble().Count == 0)
        return PaymentIssues.NoActiveCreditCardAvailable;

    return null;
}

public virtual void Add(CreditCard creditCard)
{
    this.creditCards.Add(creditCard);
    
    DomainEvents.Raise<CreditCardAdded>(new CreditCardAdded() { CreditCard = creditCard });
}

internal virtual void Add(Purchase purchase)
{
    this.purchases.Add(purchase);
}
}

如果我要重构上面的代码,我会改变什么?

  • 删除 PurchasesCounty 属性。我永远不需要访问 County 属性,所以为什么还要引用它?每次加载 Customer 时都不会访问购买列表,所以根本没有必要将其放在 customer 对象上。是的,你可以使用延迟加载。但为什么要在一个对象中包含你实际上并不需要的属性呢?
  • 移动和重构 IsPayReadyIsPayReady 是结账上下文的一部分。我不认为它应该放在 customer 对象内部。所以我创建了 CheckoutService,这是一个领域服务,它检查 customer 是否可以付款,product 是否可以销售,然后执行 purchase
  • 删除 Cart 属性。Cart 是一个聚合根,可以在需要时直接访问,同样,没有必要每次都与 Customer 一起加载它。
  • 删除 CreatedActive 属性。除非你的业务分析师说你需要将这两个属性保留在你的领域中,因为它们是业务领域的一部分,否则你不应该保留它们。此外,我已经将 领域事件日志记录 引入了我的代码库,所以根本没有必要用对象生命周期信息来污染你的领域模型,因为这些信息已经包含在事件中了。

现在 Customer 聚合根看起来是这样的

public class Customer : IAggregateRoot
{
    private List<CreditCard> creditCards = new List<CreditCard>();

    public virtual Guid Id { get; protected set; }
    public virtual string FirstName { get; protected set; }
    public virtual string LastName { get; protected set; }
    public virtual string Email { get; protected set; }
    public virtual string Password { get; protected set; }
    public virtual decimal Balance { get; protected set; }
    public virtual Guid CountryId { get; protected set; }

    public virtual ReadOnlyCollection<CreditCard> CreditCards 
        { get { return this.creditCards.AsReadOnly(); } }

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail() { Customer = this });
        }
    }

    public static Customer Create(string firstname, string lastname, string email, Country country)
    {
        return Create(Guid.NewGuid(), firstname, lastname, email, country); ;
    }

    public static Customer Create
        (Guid id, string firstname, string lastname, string email, Country country)
    {
        if (string.IsNullOrEmpty(firstname))
            throw new ArgumentNullException("firstname");

        if (string.IsNullOrEmpty(lastname))
            throw new ArgumentNullException("lastname");

        if (string.IsNullOrEmpty(email))
            throw new ArgumentNullException("email");

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

        Customer customer = new Customer()
        {
            Id = id,
            FirstName = firstname,
            LastName = lastname,
            Email = email,
            CountryId = country.Id
        };

        DomainEvents.Raise<CustomerCreated>(new CustomerCreated() { Customer = customer });

        return customer;
    }

    public virtual ReadOnlyCollection<CreditCard> GetCreditCardsAvailble()
    {
        return this.creditCards.FindAll
            (new CreditCardAvailableSpec(DateTime.Today).IsSatisfiedBy).AsReadOnly();
    }

    public virtual void Add(CreditCard creditCard)
    {
        this.creditCards.Add(creditCard);

        DomainEvents.Raise<CreditCardAdded>(new CreditCardAdded() { CreditCard = creditCard });
    }
}

为什么保持聚合根扁平化是个好主意?

  • 更好的性能,因为你加载的数据更少
  • 更容易单元测试
  • 更易于维护和扩展
  • 需要更少的 ORM 配置
  • 更小的事务,意味着更小的锁,这意味着更好的吞吐量

为了这篇文章,我已经重构了我 Github 上的 "领域驱动设计示例" 仓库。如果您想查看所有更改,请点击这里

3. 领域模型 != 读取模型

在领域模型方面,查询可能会很棘手。我记得当有一个需求需要将更多数据投影到 UI 上时。那真是太痛苦了。我不得不扩展我的聚合根并向其添加额外的属性,这违反了最佳实践第 1 和第 2 条。为了提高性能,我花了几个小时调整 NHibernate 配置。这是错误的。我应该只将我的聚合根用于命令,而应该使用数据库视图来投影数据。我之所以这样做,是因为我当时认为使用 DDD 我应该在所有地方都使用领域模型。只需使用 Specification pattern 并遍历模型,问题就解决了,SQL 已死,Specification pattern 是未来的方向。这是天真的。

对于复杂的 API 或 UI 投影,你应该使用 Read 模型。Read 模型并不可怕,它可以很简单,就像数据库视图一样。如果你想看更多示例,请点击这里

另外,我建议开发者在开始使用 DDD 时,避免使用 Specification Pattern,它弊大于利。

4. 领域事件是不可变的既定事实

许多人并不完全理解领域事件。他们认为领域事件是一个交互式接口。Domain 事件 CustomerCheckedOut 被触发。处理程序接收到它,加载另一个聚合根,而另一个聚合根可以拒绝 CustomerCheckedOut 领域事件。Domain 事件不是一个交互式接口。Domain 事件只是一个不可变的既定事实。Domain 事件处理程序监听事件,所有这些处理程序所能做的就是改变其他聚合根、发送电子邮件等。处理程序不能改变领域事件,也不能改变已触发领域事件的聚合根。你不应该使用 domain 事件来请求执行某事的权限,例如,CanCustomerCheckout 领域事件询问整个系统:这个 customer 可以结账吗?没问题吗?

你什么时候会使用 Domain Events?当你需要告诉你的 Domain 模型中的其他组件某事已经发生时,这是一个让某个处理程序采取行动的机会。例如,处理程序会发送电子邮件,它会加载一个业务计数器并增加它以进行分析,它会重新同步某些有界上下文的数据。

5. 聚合根不能访问存储库

总的来说,只有应用程序服务、领域服务和领域事件处理程序才能访问你的存储库接口。聚合根不应该访问存储库。

© . All rights reserved.