实体

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

<?php

//entity
class OrderLine {
    function __construct(
        public readonly string $sku,
        public readonly int $price,
        public readonly int $count
    ) {
        if ($count <= 0) {
            throw new InvalidArgumentException("bad count");
        }
    }

    function cost(): int {
        return $this->price * $this->count;
    }
}

//entity
class Order {
    private array $lines = [];
    private bool $paid = false;

    function __construct(public readonly int $id) {}

    function add(string $sku, int $price,
                 int $count): void {
        $this->lines[] = new OrderLine($sku, $price,
                                       $count);
    }

    function total(): int {
        $sum = 0;
        foreach ($this->lines as $line) {
            $sum += $line->cost();
        }
        return $sum;
    }

    //business rule: 10% off from 200
    function discount(): int {
        $total = $this->total();
        return $total >= 200 ? intdiv($total, 10) : 0;
    }

    function payable(): int {
        return $this->total() - $this->discount();
    }

    //business rule: an empty order is never paid
    function pay(): void {
        if ($this->lines === []) {
            throw new LogicException("empty order");
        }
        $this->paid = true;
    }

    function paid(): bool {
        return $this->paid;
    }

    function size(): int {
        return count($this->lines);
    }
}

//client code
//no database and no screen anywhere above
$order = new Order(1001);
$order->add("pen", 30, 2);
$order->add("lamp", 190, 1);
echo "order ", $order->id, "\n";
echo "lines ", $order->size(), "\n";
echo "total ", $order->total(), "\n";
echo "discount ", $order->discount(), "\n";
echo "payable ", $order->payable(), "\n";
$order->pay();

//echo shows a bool as 1 and as nothing at all, so a
//bool goes through var_export
echo "paid ", var_export($order->paid(), true), "\n";

try {
    (new Order(1002))->pay();
} catch (LogicException $error) {
    echo "refused: ", $error->getMessage(), "\n";
}

try {
    $order->add("chair", 15, 0);
} catch (InvalidArgumentException $error) {
    echo "refused: ", $error->getMessage(), "\n";
}
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid true
//         refused: empty order
//         refused: bad count


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。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。

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

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