网站首页 > 博客文章 正文
值对象是领域驱动设计的构建块之一。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 的值
带走
值对象有助于设计丰富的域模型。您可以使用它们来解决原始痴迷并封装域不变量。值对象可以通过防止无效域对象的实例化来减少错误。
可以使用 或 基类来表示值对象。这取决于具体要求和域的复杂性。我默认使用记录,除非我需要基类的某些特性。例如,当您想要控制相等组件时,基类是实用的。
猜你喜欢
- 2024-10-15 Python 速度慢,试试这个方法提高 1000 倍
- 2024-10-15 C# 文件操作浅析(c#代码文件)
- 2024-10-15 从零开始自学C#基础的第十五天——数组的基本用法
- 2024-10-15 浅谈C#取消令牌CancellationTokenSource
- 2024-10-15 总结了才知道,原来channel有这么多用法
- 2024-10-15 面向对象(8-15)异常类-C#编程零基础到入门学习
- 2024-10-15 .NET 6 中 LINQ 的改进(.net 调优)
- 2024-10-15 Log4net配置文件 C#(c# log4j)
- 2024-10-15 C# BIN文件读取以及CRC校验(匹配STM32F103)
- 2024-10-15 C#如何对String中的Contact/Join方法进行优化的
你 发表评论:
欢迎- 最近发表
-
- 给3D Slicer添加Python第三方插件库
- Python自动化——pytest常用插件详解
- Pycharm下安装MicroPython Tools插件(ESP32开发板)
- IntelliJ IDEA 2025.1.3 发布(idea 2020)
- IDEA+Continue插件+DeepSeek:开发者效率飙升的「三体组合」!
- Cursor:提升Python开发效率的必备IDE及插件安装指南
- 日本旅行时想借厕所、买香烟怎么办?便利商店里能解决大问题!
- 11天!日本史上最长黄金周来了!旅游万金句总结!
- 北川景子&DAIGO缘定1.11 召开记者会宣布结婚
- PIKO‘PPAP’ 洗脑歌登上美国告示牌
- 标签列表
-
- ifneq (61)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)