域驱动设计和ORM限制

aci*_*cid 5 php orm domain-driven-design doctrine-orm

我看到的大多数DDD示例都是用Java编写的,绝大多数使用Hibernate来持久化和获取实体.我真的没有任何经验,我假设Hibernate是一个足以解决依赖关系,处理值对象等的工具.我选择的ORM是Doctrine2,据我所知这是PHP目前最好的工具,但我认为支持DDD原则还不够.

以下是域层的示例:

/**
 * Simple value object
 */
class ProductId
{
    private $value;

    function __construct($value)
    {
        $this->value = $value;
    }

    public function value()
    {
        return $this->value;
    }
}

/**
 * Example dependency
 */
class Dependency
{
    public function doNothing()
    {

    }
}

/**
 * Game class done in a DDD manner
 */
class Game
{
    /**
     * @var ProductId
     */
    private $id;

    /**
     * @var string
     */
    private $title;

    /**
     * @var Dependency
     */
    private $dependency;

    function __construct(ProductId $id, $title, Dependency $dependency)
    {
        $this->id    = $id;
        $this->title = $title;

        // Validation
        Assertion::minLength(25, $title);
    }

    /**
     * @return ProductId
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    public function someBehavior()
    {
        $this->dependency->doNothing();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在使用Doctrine,您可以使用XML或YAML映射映射Game到某个表.然而,在调用$gameRepository->productOfId($someId);你会得到的是一个格式错误的对象.原因如下:

  1. 由于Doctrine2不支持的值对象,打电话时getId(),你会得到一个简单的int,因为它的映射将指向一个整数列.最新的beta有点支持它们,但它仍然不是很灵活,在更复杂的场景中也不容易配置.
  2. 由于Doctrine2在调用someBehavior()获取的实体时创建用于检索的代理对象,因此代理对象无法解析构造函数依赖性,因此会出现致命错误.这可以通过创建一些DomainRegistry单例并从那里获得依赖来实际克服,但我真的不喜欢那些暗示在我身上.但是我们仍在跳过验证,因为没有调用构造函数,我不会仅仅依赖于数据库完整性.

我应该如何克服这个问题?我宁愿不在if is int return ProductId(int)我的域模型中使用一些东西,因为我希望我的域层是持久性 - 无知的.

我想到的一件事是(假设我会坚持使用ORM,而不仅仅是DBAL)来处理像DTO这样的Doctrine实体(我希望我在这里正确使用该术语)并从中组装域对象.所以除了提到的课程,我还有类似的东西:

/**
 * Doctrine entity treated like a pure value container
 */
class GameDTO
{
    private $id;
    private $title;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }
}

class DoctrineGameRepository
{
    /**
     * @var Dependency
     */
    private $dependency;

    /**
     * Doctrine's entity manager
     */
    private $em;

    function __construct(Dependency $dependency, $em)
    {
        $this->dependency = $dependency;
        $this->em = $em;
    }

    public function productOfId($id)
    {        
        /** @var GameDTO $gameDto */
        $gameDto = $this->em->find($id);

        return new Game($gameDto->getId(), $gameDto->getTitle(), $this->dependency);        
    }
}
Run Code Online (Sandbox Code Playgroud)

这样我就克服了基础设施的限制,但它增加了另一层复杂性.另外,我应该如何处理需要反映到我的DTO的对象的更改,以便我可以更新我的数据库?

我的第二个想法是改编CQRS,但这对我来说只是一个流行语,因为我还没有对它进行太多的研究.我知道它的原则,但我不知道我应该为哪个类和存储库建模.

你会如何处理所有这些?

Sud*_*han 3

我认为这个问题提出了一个很好的观点。

ORM 的限制是否会影响您的 DDD 设计?

我大部分时间都用java编写代码,我知道hibernate是如何演变的,3-4年前甚至hibernate都不支持值对象,对嵌入对象的支持也很幼稚,看起来Doctrine2正处于那个阶段

我认为一旦你倾向于 CQRS,ORM 的魔力就不再那么需要了,我认为 CQRS 和 DDD 齐头并进,CQRS如果单独实现的话非常简单,只有当 ES 添加到方程中时它才会变得复杂( ES 有它的好处)。

因此,我建议您对所有想要使用 DDD 概念的项目都使用基本的 CQRS。因此,从这里我们可以假设您有一个 Finder/Query 类来处理您的查询,这些可能应该是直接的 SQL

不让我们深入了解存储库的实际内容。一旦我们从存储库中查询出来,存储库实际上很简单,它公开的接口将非常严格,这里是一个例子。

public interface Repository<T> {

    T load(Object aggregateIdentifier);    
    void add(T aggregate);
    void update (T aggregate);
}
Run Code Online (Sandbox Code Playgroud)

您可以为系统中的每个聚合实现一个实现。

在这里,您可以按照您认为合适的方式将聚合从结果集中编组出来。这可能需要付出一些努力,但成本可以在各个项目中分摊,并且您可以免受所有 ORM 黑魔法的影响:)