专业的编程技术博客社区

网站首页 > 博客文章 正文

.NET 中的值对象(DDD 基础知识)(.net的数据类型)

baijin 2024-10-15 08:31:49 博客文章 9 ℃ 0 评论

值对象是领域驱动设计的构建块之一。DDD 是一种用于解决复杂领域问题的软件开发方法。

Value 对象封装了一组基元值和相关的不变量。例如 money 和 date range 对象。货币由金额和货币组成。日期范围由开始日期和结束日期组成。

什么是值对象?

《领域驱动设计》中的定义为:

表示域的描述性方面且没有概念标识的对象称为值对象。值对象被实例化以表示我们只关心它们是什么的设计元素,而不关心它们是谁或它们是什么。

— 埃里克·埃文斯

值对象与实体不同,它们没有标识的概念。它们将原始类型封装在域中,并解决了原始痴迷。

值对象有两个主要特性:

  • 它们是不可变的
  • 他们没有身份

价值对象的另一个特性是结构平等。如果两个值对象的值相同,则它们相等。这种质量在实践中是最不重要的。但是,在某些情况下,您只需要某些值来确定相等性。

实现值对象

价值对象最重要的特性是不变性。创建对象后,值对象的值无法更改。如果要更改单个值,则需要替换整个值对象。

下面是一个实体,其基元值表示地址以及预订的开始和结束日期。Booking

public class Booking
{
    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string Country { get; init; }
    public string ZipCode { get; init; }

    public DateOnly StartDate { get; init; }
    public DateOnly EndDate { get; init; }
}

可以将这些基元值替换为 和 value 对象。AddressDateRange

public class Booking
{
    public Address Address { get; init; }

    public DateRange Period { get; init; }
}

但是如何实现值对象呢?

C# 记录

可以使用 C# 记录来表示值对象。记录在设计上是不可变的,并且它们具有结构上的相等性。我们希望我们的价值对象同时具备这两种品质。

例如,可以使用带有主构造函数的 来表示值对象。这种方法的优点是简洁。Addressrecord

public record Address(
    string Street,
    string City,
    string State,
    string Country,
    string ZipCode);

但是,在定义私有构造函数时,您将失去优势。当我们希望在创建值对象时强制执行不变量时,就会发生这种情况。使用记录的另一个问题是使用表达式避免值对象不变性。

public record Address
{
    private Address(
        string street,
        string city,
        string state,
        string country,
        string zipCode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipCode;
    }

    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string Country { get; init; }
    public string ZipCode { get; init; }

    public static Result<Address> Create(
        string street,
        string city,
        string state,
        string country,
        string zipCode)
    {
        // Check if the address is valid

        return new Address(street, city, state, country, zipCode);
    }
}

基类

实现值对象的另一种方法是使用基类。基类使用抽象方法处理结构相等性。 实现必须实现此方法并定义相等组件。

使用基类的优点是它是显式的。很清楚域中的哪些类表示值对象。另一个优点是能够控制相等分量。

这是我在项目中常用的基类:ValueObject

public abstract class ValueObject : IEquatable<ValueObject>
{
    public static bool operator ==(ValueObject? a, ValueObject? b)
    {
        if (a is null && b is null)
        {
            return true;
        }

        if (a is null || b is null)
        {
            return false;
        }

        return a.Equals(b);
    }

    public static bool operator !=(ValueObject? a, ValueObject? b) =>
        !(a == b);

    public virtual bool Equals(ValueObject? other) =>
        other is not null && ValuesAreEqual(other);

    public override bool Equals(object? obj) =>
        obj is ValueObject valueObject && ValuesAreEqual(valueObject);

    public override int GetHashCode() =>
        GetAtomicValues().Aggregate(
            default(int),
            (hashcode, value) =>
                HashCode.Combine(hashcode, value.GetHashCode()));

    protected abstract IEnumerable<object> GetAtomicValues();

    private bool ValuesAreEqual(ValueObject valueObject) =>
        GetAtomicValues().SequenceEqual(valueObject.GetAtomicValues());
}

值对象实现如下所示:Address

public sealed class Address : ValueObject
{
    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string Country { get; init; }
    public string ZipCode { get; init; }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

何时使用值对象?

使用值对象来封装域不变量。封装是任何领域模型的一个重要方面。不应能够创建处于无效状态的值对象。

值对象还为您提供了类型安全性。看看这个方法签名:

public interface IPricingService
{
    decimal Calculate(Apartment apartment, DateOnly start, DateOnly end);
}

然后,将其与此方法签名进行比较,其中添加了值对象。可以看到 with value 对象如何更加明确并获得类型安全的好处。编译代码时,值对象可减少错误蔓延的可能性。

public interface IPricingService
{
    PricingDetails Calculate(Apartment apartment, DateRange period);
}

在决定是否需要值对象时,还应考虑以下几点:

  • 不变量的复杂性 — 如果强制执行复杂的不变量,请考虑使用值对象
  • 基元数 — 值对象在封装许多基元值时是有意义的
  • 重复次数 — 如果只需要在代码中的几个位置强制实施不变性,则可以管理无值对象

使用 EF Core 保存值对象

值对象是域实体的一部分,您需要将它们保存在数据库中。

下面演示如何使用 EF 拥有的类型和复杂类型来保存值对象。

拥有的类型

可以通过在配置实体时调用该方法来配置拥有的类型。这会使用EF 将 and value 对象保存到与实体相同的表中。值对象用表中的附加列表示。

public void Configure(EntityTypeBuilder<Apartment> builder)
{
    builder.ToTable("apartments");

    builder.OwnsOne(property => property.Address);

    builder.OwnsOne(property => property.Price, priceBuilder =>
    {
        priceBuilder.Property(money => money.Currency)
            .HasConversion(
                currency => currency.Code,
                code => Currency.FromCode(code));
    });
}

关于拥有类型的更多评论:

  • 拥有的类型具有隐藏的键值
  • 不支持可选(可为 null)拥有的类型
  • 自有集合受以下支持:OwnsMany
  • 表拆分允许您单独保留拥有的类型

复杂类型

复杂类型是 .NET 8 中提供的一项新的 EF 功能。它们不是由键值标识或跟踪的。复杂类型必须是实体类型的一部分。

复杂类型更适合使用 EF 表示值对象。

下面介绍如何将值对象配置为复杂类型:Address

public void Configure(EntityTypeBuilder<Apartment> builder)
{
    builder.ToTable("apartments");

    builder.ComplexProperty(property => property.Address);
}

复杂类型的一些限制:

  • 不支持集合
  • 不支持可为 null 的值

带走

值对象有助于设计丰富的域模型。您可以使用它们来解决原始痴迷并封装域不变量。值对象可以通过防止无效域对象的实例化来减少错误。

可以使用 或 基类来表示值对象。这取决于具体要求和域的复杂性。我默认使用记录,除非我需要基类的某些特性。例如,当您想要控制相等组件时,基类是实用的。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表