Easy📖Теория2 min

Шаг 5: Value Objects -- Money, Address, Email

Создание Value Objects: Money с арифметикой и валютой, ShippingAddress с валидацией, Email с проверкой формата, перечисление OrderStatus

Шаг 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 без города.

Проверь себя

🧪

Почему Money возвращает новый объект из метода Add, а не изменяет текущий?

🧪

Что означает yield return в методе GetEqualityComponents?

🧪

Зачем Value Objects хранят приватный конструктор без параметров?