DDD,PHP。领域对象和业务逻辑

8

最近,我一直在努力理解DDD和模型层的概念。阅读了大量的文章、示例、问答,花费了很多时间。但我仍然不确定是否掌握了某些原则。

其中一个问题的答案是:领域对象中应该存在多少业务逻辑?有些资料说领域对象应该附带整个业务逻辑,另一方面,我也看到过一些文章认为它应该尽可能小,只表示其值。这让我感到很困惑。

在我的理解中,领域对象是代表领域中实体的类。

例如,我们来看看发票实体。每张发票都包含其项目。为了计算发票值,我们必须对所有项目的值进行求和(这是非常简单的例子,在实际世界中会出现添加税、计算已付金额等情况)。

class Invoice
{
    public $id;
    public $items = [];
    public $status;

    const STATUS_PAID = 'paid';
    const STATUS_NOT_PAID = 'not_paid';

    public function isPaid()
    {
        return $this->status == self::STATUS_PAID;
    }

    public function getInvoiceValue()
    {
        $sum = 0;
        foreach($this->items as $item) {
            $sum += $item->value;
        }
        return $sum;
    }
}

我理解,isPaid() 方法是在正确的位置。它引用自己的数据。但是我对 getInvoiceValue() 不确定。这里我们操作的是其他领域对象。

也许我们应该仅使用领域对象来表示数据,但使用一些修饰器来执行更高级的任务?

提前感谢。

3个回答

4

领域对象中应该存在多少业务逻辑?[...] 我看过一些文章,认为它应该尽可能小,只代表其值。

要注意贫血的领域模型,它几乎完全由数据组成,缺乏行为。DDD的重点是创建一个充满行为的领域模型。因此,在领域类中添加逻辑是可以的。

DDD强调良好的面向对象设计,将方法和数据放在一起,从而促进高度内聚的系统


谢谢您的回答。如果我们将大量业务逻辑附加到单个域对象上,是否会与SRP相冲突? - hopsey
1
DDD希望实现良好的OO设计。遵循单一职责原则和其他明智的OO实践。 - Markus Pscheidt
1
@hopsey 请记住,您的领域不仅仅是领域对象。业务逻辑不仅限于存在于聚合根和实体中。它们可以存在于多个地方,包括领域服务、命令等。领域是代表普遍语言的一组连贯的类。没有规则(缺乏更好的词),规定业务规则必须存在于聚合/实体中,而不能存在于其他地方。其中很大一部分取决于您特定的实现和设计决策。 - Joseph Ferris

3
领域对象中应该包含多少业务逻辑?
尽可能全部都要包含。DDD的目的是捕捉您的业务逻辑在您的领域中,各种战术模式可以用于帮助(聚合、实体、值对象、领域服务等)。
领域对象是表示领域实体的类。
您的领域中的类可以代表不仅仅是实体。聚合、实体、值对象、领域服务等都可以由您领域中的类来表示。
但我不确定getInvoiceValue()。我们在这里处理其他领域对象。
您提供的发票示例是聚合的经典示例-发票将包含发票项目。在这里使用getInvoiceValue()是可以的。
在我们的情况下,发票是一个聚合。聚合根是发票本身,但它也是实体,对吗?它有自己的身份(唯一的发票号码)。
是的,正确的。
那么发票项目是什么?我可以直接从发票项目存储库中获取它们(如果我应该创建这样的存储库),还是我只能仅在聚合上操作?
这取决于您的用例。将写和读模型分开(CQRS)有所帮助。如果您谈论读取侧(即报告),则可以绕过领域模型并拥有代表读取模型的对象。这可能只是一个数据库查询。如果您谈论写入侧(即命令、领域),则通常会为每个聚合根拥有一个存储库。聚合根是建模问题。您要以这样的方式创建它们,以强制执行所有业务规则-在您的示例中,如果发票需要加载发票项目来执行规则(例如“每张发票不超过5个项目”),那么是的,它们应该通过聚合根加载。

谢谢您的帮助回答。所以 - 让我们确认一下我的理解是否正确。在我们的情况下,发票是一个聚合体。聚合根是发票本身,但它也是实体,对吗?它有自己的身份(唯一的发票号码)。那么发票项目是什么?我可以直接从InvoiceItem存储库中获取它们(如果我应该创建这样的存储库),还是我只能始终操作聚合体? - hopsey

3
我不确定这类问题是否有一个“正确”的答案,因为应用DDD取决于你应用它的具体领域。如果它满足业务需求,那么你的实现在某些地方可能是完全有效的。但在像你提到的税收等其他情况下,则不是。所以我建议,在将其转换为代码之前,您需要继续就您的领域提出问题,以充分了解您的需求。
话虽如此,如果你有一个更复杂的场景需要一些额外的关于外部世界的知识才能得出发票的价值,那么一个选项是在你的领域中明确表示它。在你的例子中,这可以是一个InvoiceProducer,它可以拥有以下签名:
class InvoiceProducer {

    public function __construct(TaxProvider $taxProvider) {
        $this->taxProvider = $taxProvider;
    }

    public function invoiceFor(array $items) {
        new Invoice($items, $this->calculateValue($items));
    }

    private function calculateValue(array $items) {
        $sum = array_reduce($items, function($acc, $item){
            $acc += $item->value;
        }

        return $this->taxProvider->applyTaxTo($sum);
    }
}

另一种选择是使用某种策略模式,这将使您的实现与现在的方式非常相似,但您将通过调用传递您希望计算税收的方式:
public function getInvoiceValue(TaxProvider $taxProvider)
{
    $sum = 0;
    foreach($this->items as $item) {
        $sum += $item->value;
    }

    return $taxProvider->applyTaxFor($sum);
}

再次强调,这真的取决于您特定的域名是如何工作的,但是正如您所看到的,实现并不是什么大问题。更多的是关于如何让它在您的域名中合适地运作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接