Шаг 5: Value Objects
Value Object -- объект без идентичности, определяемый значениями. Money(100, "RUB") == Money(100, "RUB") всегда true. Value Object неизменяемый -- операции создают новые объекты.
OrderStatus (перечисление)
В папке Enums создайте OrderStatus.cs:
namespace OrderManagement.Domain.Enums;
public enum OrderStatus
{
Draft = 0,
Confirmed = 1,
Paid = 2,
Processing = 3,
Shipped = 4,
Delivered = 5,
Cancelled = 6
}
Числа = 0, 1, ... используются при хранении в БД. enum гарантирует: нельзя опечататься в строке статуса, компилятор проверит.
Money.cs
В папке ValueObjects:
using OrderManagement.Domain.Common;
using OrderManagement.Domain.Exceptions;
namespace OrderManagement.Domain.ValueObjects;
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
private Money() { }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new DomainException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency))
throw new DomainException("Currency is required");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
public static Money Zero(string currency = "RUB") =>
new(0, currency);
public Money Add(Money other)
{
EnsureSameCurrency(other);
return new Money(Amount + other.Amount, Currency);
}
public Money Subtract(Money other)
{
EnsureSameCurrency(other);
var result = Amount - other.Amount;
if (result < 0)
throw new DomainException("Result cannot be negative");
return new Money(result, Currency);
}
public Money Multiply(int quantity) =>
new(Amount * quantity, Currency);
private void EnsureSameCurrency(Money other)
{
if (Currency != other.Currency)
throw new DomainException(
$"Cannot operate on different currencies: " +
$"{Currency} and {other.Currency}");
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public override string ToString() => $"{Amount:F2} {Currency}";
}
Add() не изменяет текущий объект -- создаёт новый. Приватный конструктор без параметров нужен для EF Core (рефлексия при чтении из БД). ToUpperInvariant() нормализует валюту: "rub" -> "RUB".
ShippingAddress.cs
using OrderManagement.Domain.Common;
using OrderManagement.Domain.Exceptions;
namespace OrderManagement.Domain.ValueObjects;
public class ShippingAddress : ValueObject
{
public string Street { get; }
public string City { get; }
public string PostalCode { get; }
public string Country { get; }
private ShippingAddress() { }
public ShippingAddress(string street, string city,
string postalCode, string country)
{
if (string.IsNullOrWhiteSpace(street))
throw new DomainException("Street is required");
if (string.IsNullOrWhiteSpace(city))
throw new DomainException("City is required");
if (string.IsNullOrWhiteSpace(postalCode))
throw new DomainException("Postal code is required");
if (string.IsNullOrWhiteSpace(country))
throw new DomainException("Country is required");
Street = street; City = city;
PostalCode = postalCode; Country = country;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return PostalCode;
yield return Country;
}
public override string ToString() =>
$"{Street}, {City}, {PostalCode}, {Country}";
}
Принцип: если Value Object создан -- он валиден. Невозможно получить Money с отрицательной суммой или ShippingAddress без города.