实施规格模式。

6

尝试使用规范模式并遇到在不同实现(例如内存,orm等)中使其工作的问题。 我的主要ORM是Doctrine,这意味着我的第一选择是使用Criterias作为规范,因为它们适用于ArrayCollections(对于InMemory实现)和ORM。 不幸的是,它们在可以运行的查询类型方面相当有限(无法执行连接操作)。

例如,假设我有一个UserHasBoughtProduct规范,构造函数中给出一个产品ID。 规范非常容易以天真的水平编写。

public function isSpecifiedBy(User $user)
{
    foreach ($user->getProducts() as $product)
    {
        if ($product->getId() == $this->productId)
        {
            return true;
        }
    }

    return false;
}

然而,如果我想找出所有购买过该产品的用户怎么办? 我需要通过某种findSpecifiedBy(Specification $specification)方法将此规范传递给我的UserRepository。 但是这在生产中行不通,因为它会检查数据库中的每个用户。

接下来我的想法是规范应该只是一个接口,实现应该由基础设施处理。 因此,在我的persistence\doctrine\user\目录中,我可能会有一个UserHasBoughtProduct规范,在我的persistence\InMemory\user目录中还有另一个规范。 这在某种程度上可以工作,但在代码中使用时非常麻烦,因为我需要通过DI容器或某种工厂来提供所有规范。更不用说如果我有一个需要多个规范的类,我需要通过构造函数注入它们全部。感觉很糟糕。

如果我能在一个方法中简单地执行以下操作,那将更可取:

$spec = new UserHasBoughtProductSpecification($productId);
$users = $this->userRepository->findSatisfiedBy($spec);
//or
if ($spec->isSatisfiedby($user))
{
//do something
}

有没有人在 PHP 中做过这个?你是如何实现规范模式的,使其在现实世界中可用于不同的后端,比如内存、ORM、纯 SQL 或其他任何东西?

1个回答

13

如果您在域中将Specification声明为接口,并在基础设施中实现它,那么您正在将业务规则移动到基础设施中。这与DDD的原则相反。

因此,Specification业务规则必须放置在域层中。

Specification用于验证对象时,非常有效。问题出现在使用它来从集合中选择一个对象,即从Repository中选择对象,由于内存中可能存在大量对象。

为了避免将业务规则嵌入到Repository中并泄漏SQL细节到Domain中,Eric Evans在他的DDD书中给出了几个解决方案:

1.双重分派+专门查询

    public class UserRepository()
    {
        public function findOfProductIdBought($productId)
        {
            // SQL
            $result = $this->execute($select);

            return $this->buildUsersFromResult($result);
        }    

        public function selectSatisfying(UserHasBoughtProductSpecification $specification)
        {
            return $specification->satisfyingElementsFrom($this);
        }
    }


    public class UserHasBoughtProductSpecification()
    {
        // construct...

        public function isSatisfyBy(User $user)
        {
            // business rules here...
        }

        public function satisfyingElementsFrom($repository)
        {
            return $repository->findOfProductId($this->productId);
        }
    }

Repository有一种专门的查询,与我们的Specification完全匹配。 虽然这种类型的查询可以被接受,但E. Evans指出它最有可能只在这种情况下使用。

2. 双重分派+通用查询

另一个解决方案是使用更通用的查询。

public class UserRepository()
{
    public function findWithPurchases()
    {
        // SQL
        $result = $this->execute($select);

        return $this->buildUsersFromResult($result);
    }    

    public function selectSatisfying(UserHasBoughtProductSpecification $specification)
    {
        return $specification->satisfyingElementsFrom($this);
    }
}


public class UserHasBoughtProductSpecification()
{
    // construct ...

    public function isSatisfyBy(User $user)
    {
        // business rules here...
    }

    public function satisfyingElementsFrom($repository)
    {
        $users = $repository->findWithPurchases($this->productId);

        return array_filter($users, function(User $user) {
            return $this->isSatisfyBy($user);
        });
    }
}

两种解决方案:

  • 将业务规则放在一个地方,即领域层。
  • 将 SQL 放在仓储层。
  • 规范控制应使用哪个查询。
  • 过滤器设置从仓库返回的结果(部分或全部)。

我理解这两者之间的原因。虽然都不是“理想”的,但是在领域/模型中保持业务逻辑似乎需要以这种方式设置。感谢您的建议。 - Anti-Dentite
1
你如何高效地处理复合规范?规范的一个优点是它们很容易组合。我有一些想法,但我很想听听你的想法。 - plalx
@plalx请跟随此链接https://github.com/dddinphp/ddd/pull/3/files,其中包含基于Fowler和Evans论文的PHP实现。 - martinezdelariva
@martinezdelariva 我的意思是从代码仓库的角度来看。 - plalx
1
我对你的第一个例子“双重分派+专门查询”很好奇,“isSatisfiedBy”的用途是什么?我在例子中没有看到它被使用。 - Jaime Sangcap
显示剩余2条评论

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