实体

设计模式 / 整洁架构模式
image

using System;
using System.Collections.Generic;
using System.Linq;

//client code
//no database and no screen anywhere below
var order = new Order(1001);
order.Add("pen", 30, 2);
order.Add("lamp", 190, 1);
Console.WriteLine($"order {order.Id}");
Console.WriteLine($"lines {order.Lines.Count}");
Console.WriteLine($"total {order.Total}");
Console.WriteLine($"discount {order.Discount}");
Console.WriteLine($"payable {order.Payable}");
order.Pay();
Console.WriteLine($"paid {order.Paid}");

//every rule below is refused by the entity itself
try
{
    order.Add("pen", 30, 1);
}
catch (InvalidOperationException error)
{
    Console.WriteLine($"refused: {error.Message}");
}

try
{
    new Order(1002).Pay();
}
catch (InvalidOperationException error)
{
    Console.WriteLine($"refused: {error.Message}");
}

try
{
    new OrderLine("chair", 30, 0);
}
catch (ArgumentException error)
{
    Console.WriteLine($"refused: {error.Message}");
}
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid True
//         refused: already paid
//         refused: empty order
//         refused: bad count

//entity: a record still guards its own rule, because a
//positional property may be re-declared with a check
record OrderLine(string Sku, int Price, int Count)
{
    public int Count { get; } = Count > 0
        ? Count
        : throw new ArgumentException("bad count");

    public int Cost => Price * Count;
}

//entity
sealed class Order
{
    private readonly List<OrderLine> lines = [];

    public Order(int id) => Id = id;

    public int Id { get; }

    public IReadOnlyList<OrderLine> Lines => lines;

    public bool Paid { get; private set; }

    //business rule: a paid order never changes
    public void Add(string sku, int price, int count)
    {
        if (Paid)
        {
            throw new InvalidOperationException(
                "already paid");
        }
        lines.Add(new OrderLine(sku, price, count));
    }

    public int Total => lines.Sum(line => line.Cost);

    //business rule: 10% off from 200
    public int Discount => Total >= 200 ? Total / 10 : 0;

    public int Payable => Total - Discount;

    //business rule: an empty order is never paid
    public void Pay()
    {
        if (lines.Count == 0)
        {
            throw new InvalidOperationException(
                "empty order");
        }
        Paid = true;
    }
}


An entity holds a business rule of the shop and knows nothing about a database, a screen or the network, so it can be tested without them.
Read more

问题。商店对金额达到 200 的每笔订单减免 10%。如果这条规则写在网页里,又在数据库查询里写了一遍,两份副本很快就会不一致。要检查这条规则,就得启动网页或数据库。商店最重要的知识分散各处,难以测试。

思路。实体是一段数据,连同与它相关的业务规则:订单包含什么,总额如何计算,何时可以付款。它对数据库、界面或网络一无所知。整洁架构有一条主要规则,即依赖规则:外层代码可以了解内层代码,但内层代码从不了解外层代码。实体是最内层的代码,因此不依赖任何东西。

如何阅读示例。示例中有两个实体:订单行和订单本身。订单汇总各行,对 200 及以上的金额减免 10%,并计算应付金额。订单为空时,它拒绝付款。客户端代码构建含两个订单行的订单 1001,并打印其中的数字:总额 250,折扣 25,应付 225。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。

何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。

常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。