doucang9673 2015-10-25 14:49
浏览 12
已采纳

实施规范模式

Attempting to use the specification pattern and have run into the issue of having it work in different implementations (e.g., in memory, orm, etc.). My main ORM is Doctrine, which means that my first choice was to have the specifications use Criterias as they work on ArrayCollections (for InMemory implementations) and on the ORM. Unfortunately they are fairly limited in the sorts of queries they can run (can't perform a join).

As an example let's say I have a UserHasBoughtProduct specification that is given a product id in the constructor. The specification is very simple to write at a naive level.

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

    return false;
}

However, what if I wanted to find all users that had bought the product? I would need to pass this specification to my UserRepository via some sort of findSpecifiedBy(Specification $specification); method. But this doesn't work in production as it would have to check every single user in the database.

My next idea from here was that the specification be only an interface and the implementation was handled by the infrastructure. So, in my persistence\doctrine\user\ directory, I might have a UserHasBoughtProduct specification and in my persistence\InMemory\user directory I have another. This works, in a way, but is very annoying to have to use in code as I will need all of my specifications to be available either by a DI container or in some sort of factory. Not to mention that if I have a class which requires several specifications I would need to inject them all via the constructor. Smells bad.

It would be much more preferable if I could simply do the following in a method:

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

Has anyone had experience doing this in PHP? How did you manage to implement specification pattern in such a way that it works in the real world and is usable in different backends such as InMemory, ORM, pure SQL, or anything else?

  • 写回答

1条回答 默认 最新

  • douweng1904 2015-10-26 08:37
    关注

    If you declare Specification as an interface in your Domain and implement it in the infrastructure, you are moving business rules to the infrastructure. That's the opposite of what DDD does.

    So, the Specification business rules has to be place in Domain layer.

    When Specification is used to validate objects, works very well. The issue comes when is used to select an object from collection, in this case, from the Repository, due to the large number of objects in memory may be.

    In order to avoid embedding business rules into the Repository and leaked SQL details into the Domain, Eric Evans in his book of DDD, give us several solutions:

    1. Double dispatch + specialized query

        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 has a specialized query, that matches exactly with our Specification. Although this kind of query can be acceptable E. Evans points us this most likely will be used only in this case.

    2. Double dispatch + generic query

    Another solution is use more generic query.

    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);
            });
        }
    }
    

    Both solutions:

    • Keep business rules in one place, the Domain.
    • Puts SQL in the repository.
    • Specification controls what query should be used.
    • Filters set return (partial or totally) from repository.
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 素材场景中光线烘焙后灯光失效
  • ¥15 请教一下各位,为什么我这个没有实现模拟点击
  • ¥15 执行 virtuoso 命令后,界面没有,cadence 启动不起来
  • ¥50 comfyui下连接animatediff节点生成视频质量非常差的原因
  • ¥20 有关区间dp的问题求解
  • ¥15 多电路系统共用电源的串扰问题
  • ¥15 slam rangenet++配置
  • ¥15 有没有研究水声通信方面的帮我改俩matlab代码
  • ¥15 ubuntu子系统密码忘记
  • ¥15 保护模式-系统加载-段寄存器