如何为ORM胖领模型减肥?


ORM 以及保存数据的方式可以显着影响您的设计并导致胖域模型。

数据很重要,但捕获数据的方式可能会引导您走上一条需要意识到您所做的妥协的道路。

我将展示一个示例,说明并非所有数据都是平等创建的。

当您考虑一个领域并对其进行建模时,您关心什么?
大多数情况下,开发人员关注数据和数据结构。我们倾向于考虑如何保存数据,

这会影响我们的整体设计,这是理所当然的。

然而,领域建模还要求我们设计将要对公开的行为

领域模型 = 数据 + 行为

下面的示例展示了如果我们使用 ORM,实体/对象可能是什么样子。

public class WarehouseProduct
{
    public string Sku { get; private set; }
    public string Name { get; private set; }
    public string Description { get; private set; }
    public decimal Price { get; private set; }
    public int QuantityOnHand { get; private set; }

    public void SetName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("Name cannot be blank.");
        }

        Name = name;
    }

    public void SetDescription(string description)
    {
        Description = description;
    }

    public void SetPrice(decimal price)
    {
        if (price < 0)
        {
            throw new ArgumentException(
"Price must be greater than 0.");
        }

        Price = price;
    }

    public void ShipProduct(int quantity)
    {
        if (quantity > QuantityOnHand)
        {
            throw new InvalidDomainException(
"Ah... we don't have enough product to ship!");
        }

        QuantityOnHand -= quantity;
    }

    public void ReceiveProduct(int quantity)
    {
        QuantityOnHand += quantity;
    }

    public void AdjustInventory(int quantity)
    {
        if (QuantityOnHand + quantity < 0)
        {
            throw new InvalidDomainException(
"Cannot adjust to a negative quantity on hand.");
        }

        QuantityOnHand += quantity;
    }
}


查看上述示例,哪些属性与具有有意义逻辑的方法相关?

QuantityOnHand:我们使用它(状态)来确定是否可以执行给定的方法,例如 ShipProduct 和 AdjustInventory。所有其他方法都有一些逻辑来执行简单的输入验证,并且不围绕实体的状态。

QuantityOnHand 是一个与其他属性完全不同的问题。

并非实体上的所有数据都具有相同的价值、需求和效用
如果您主要考虑持久性和数据,那么您最终将看到上面实体示例。如前所述,您可能在现实世界中遇到过更大的实体。

如果您想精简或分解大型模型,您需要关注行为是什么,需要针对哪些数据/状态应用哪些业务规则

专注于这一点可以创建更精简、更集中的模型,这些模型比胖域模型更容易管理。

领域事件
我将使用事件溯源作为示例来说明这一点。您不需要进行事件溯源来使您的模型更加集中。只是它能很好地说明了这个概念。

public void ApplyEvent(IEvent evnt)
    {
        switch (evnt)
        {
            case ProductShipped shipProduct:
                Apply(shipProduct);
                break;
            case ProductReceived receiveProduct:
                Apply(receiveProduct);
                break;
            case InventoryAdjusted inventoryAdjusted:
                Apply(inventoryAdjusted);
                break;
            default:
                throw new InvalidOperationException("Unsupported Event.");
        }
    }

这是一个包含四个事件的简单事件流。我们记录了仓库中单个独特产品发生的一系列事件。

重构减肥后的新领域模型如下:

public class WarehouseProduct
{
    private readonly string _sku;
    private readonly List<IEvent> _uncommittedEvents = new();

    private int _quantityOnHand = 0;

    public WarehouseProduct(string sku)
    {
        _sku = sku;
    }

    public void ShipProduct(int quantity)
    {
        if (quantity > _quantityOnHand)
        {
            throw new InvalidDomainException("Ah... we don't have enough product to ship?");
        }

        AddEvent(new ProductShipped(_sku, quantity, DateTime.UtcNow));
    }

    private void Apply(ProductShipped evnt)
    {
        _quantityOnHand -= evnt.Quantity;
    }

    public void ReceiveProduct(int quantity)
    {
        AddEvent(new ProductReceived(_sku, quantity, DateTime.UtcNow));
    }

    private void Apply(ProductReceived evnt)
    {
        _quantityOnHand += evnt.Quantity;
    }

    public void AdjustInventory(int quantity, string reason)
    {
        if (_quantityOnHand + quantity < 0)
        {
            throw new InvalidDomainException(
"Cannot adjust to a negative quantity on hand.");
        }

        AddEvent(new InventoryAdjusted(_sku, quantity, reason, DateTime.UtcNow));
    }

    private void Apply(InventoryAdjusted evnt)
    {
        _quantityOnHand += evnt.Quantity;
    }

    public void ApplyEvent(IEvent evnt)
    {
        switch (evnt)
        {
            case ProductShipped shipProduct:
                Apply(shipProduct);
                break;
            case ProductReceived receiveProduct:
                Apply(receiveProduct);
                break;
            case InventoryAdjusted inventoryAdjusted:
                Apply(inventoryAdjusted);
                break;
            default:
                throw new InvalidOperationException(
"Unsupported Event.");
        }
    }

    private void AddEvent(IEvent evnt)
    {
        ApplyEvent(evnt);
        _uncommittedEvents.Add(evnt);
    }

    public IReadOnlyCollection<IEvent> Events() => _uncommittedEvents.AsReadOnly();
}

它所关注的只是手头的数量和围绕数量的行为。名称、描述、价格和所有其他无关的属性可以用完全不同的方式处理。在这种情况下,这可能只是一个纯粹的 CRUD 模型,不需要领域模型,因为它没有业务逻辑。

同样,这些都是平常的例子,但你可能会想到自己的系统或曾经在一个系统中使用过具有许多后备属性的大型模型,而这些属性并不都是相关的。

切分还是聚合
是把所有东西都分成小模型吗?
当然不是。

如果你的领域真的很复杂,那就把它分离成自己的模型。
你不需要一个模型来统治所有模型。

当 CRUD 和琐碎的验证都能起作用时,您也不需要使用无用的 setter 方法来强行建立 "领域模型"。

如果您使用的是具有相关实体的 ORM,那么 "肥胖的领域模型 "经常会出现在 ORM 中,因为我们关注的是如何持久化数据,而不是围绕数据的行为。

例如,如果你有一个实体,它有一个子实体列表(太多了)。如果子实体列表很多,为什么还要急于加载所有这些子实体,因为根实体上使用子实体的方法少之又少。你想在一个模型中加载它们,让相关的方法/行为使用它们。

你想要内聚