Объекты-Значения в .NET. Начало
Объектом-значением (value object) называется объект, который представляет описательный аспект предметной области и не имеет собственной идентичности. Такие объекты создаются в программе для представления элементов проекта, о которых достаточно знать только, что они собой представляют, но не каким именно предметом они являются.
— Эрик Эванс
Объекты-значения инкапсулируют примитивные типы в предметной области и решают проблему одержимости примитивами.
Основные качества:
- неизменяемость,
- отсутствие идентичности,
- структурное равенство (два объекта-значения равны, если их значения одинаковы).
Реализация
Объект Booking ниже с примитивными значениями, представляющими адрес, а также даты начала и окончания бронирования:
public class Booking
{
public string Street { get; init; }
public string City { get; init; }
public string Country { get; init; }
public DateOnly StartDate { get; init; }
public DateOnly EndDate { get; init; }
}
Можно заменить эти примитивы объектами-значениями Address и DateRange:
public class Booking
{
public Address Address { get; init; }
public DateRange Period { get; init; }
}
1. Записи
В С# для представления объектов-значений можно использовать записи. Записи неизменяемы, реализуют структурное равенство и лаконичны за счёт использования первичных конструкторов:
public record Address(
string Street,
string City,
string Country
);
Однако, при этом вы теряете возможность реализовывать инварианты (например, валидацию параметров). Также проблема инвариантов объектов-значений возникает при использовании выражения with.
2. Базовый класс
Альтернативный способ — использование базового класса ValueObject с переопределением равенства (метод GetValues). Наследники должны реализовать этот метод и определить компоненты равенства (равенство каких свойств означает равенство объектов).
Преимущество 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 && ValuesEqual(other);
public override bool Equals
(object? obj)
=> obj is ValueObject v
&& ValuesEqual(v);
public override int GetHashCode() =>
GetValues().Aggregate(
default(int),
(hash, value) =>
HashCode.Combine(hash, value.GetHashCode()));
protected abstract IEnumerable<object> GetValues();
private bool ValuesEqual(ValueObject v) =>
GetValues().SequenceEqual(v.GetValues());
}
Реализация объекта-значения Address:
public sealed class Address : ValueObject
{
public string Street { get; init; }
public string City { get; init; }
public string Country { get; init; }
protected override IEnumerable<object> GetValues()
{
yield return Street;
yield return City;
yield return Country;
}
}
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals