【问题标题】:DDD building a value object that holds a collection propertyDDD 构建一个包含集合属性的值对象
【发布时间】:2015-06-07 03:58:58
【问题描述】:

在构建一个值对象时,我有一个简单的问题,其中包含具有特定类型的值对象集合,你如何构造对象?

举个例子,假设你有一个picture,它需要多个dimensions

选项 1:

Class Picture implements valueObject{

    public function __construct(array $dimensions){
        foreach($dimensions as $dimension){
                // check if instance of `dimension` value object
        }
    }
}

选项 2:

Class Picture implements valueObject{

    public function __construct(DimensionCollection $dimensions){

    }
}

Class DimensionCollection implements Traversable{

    public function add(Dimension $dimension){
        // add to array 
    }
}

选项二偏离路线似乎更合乎逻辑,但是否有另一种模式可以更好地从 DDD 规范中获取?

【问题讨论】:

  • 好吧,除非 PHP 支持泛型,否则您的第二个选项似乎更明确。为什么顺便实现一个valueObject接口?值对象实现接口是没有意义的。
  • php中没有泛型集合,我的心也赞同选项2,界面是否只是为了澄清示例:P

标签: php domain-driven-design traversal value-objects


【解决方案1】:

您要做的第一件事是列出分析领域时想到的所有概念。我看到了:

Picture
Dimension
Dimensions

甚至,也许在 Dimensions 对象后面,您可能有一个通用(抽象)值对象 Collection 概念,它可以提供 Dimensions 以及许多其他强类型集合。

即使你可以有一个 Interfaces\Collection 来允许你域中的任何元素接受“事物的通用集合”,如果你做的很快并且不想在优化你的模型之前输入字符串。

假设 PHP 没有类模板,就像在 C++ 中一样,你不能做像 class Dimensions extends Collection<Dimension> 这样的事情,我所做的是通过严格检查输入元素来强制类型。

因此,我的集合知道它们所拥有的类是什么,并且接口通过 getItemClassName() 方法反映了这一点。

Collection 接口

这是我的基本Interfaces\Collection 的样子:

<?php

declare( strict_types = 1 );

namespace XaviMontero\ThrasherPortage\Base\Collection\Interfaces;

interface Collection extends \Countable, \IteratorAggregate, \ArrayAccess
{
    public function getItemClassName() : string;
    public function getItem( int $index );
}

注意 getItem() 确实 声明显式返回类型。我将覆盖实现接口的类中的返回类型。

基本抽象不可变Collection

这是我的基本抽象 Collection(将由其他特定于类型的类实现)的样子:

<?php

declare( strict_types = 1 );

namespace XaviMontero\ThrasherPortage\Base\Collection;

use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\ImmutabilityException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\InvalidTypeException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\OutOfRangeException;

abstract class Collection implements Interfaces\Collection
{
    // Based on http://aheimlich.dreamhosters.com/generic-collections/Collection.phps

    protected $itemClassName;
    protected $items = [];

    /**
     * Creates a new typed collection.
     * @param string $itemClassName string representing the class name of the valid type for the items.
     * @param array $items array with all the objects to be added. They must be of the class $itemClassName.
     */
    public function __construct( string $itemClassName, array $items = [] )
    {
        $this->itemClassName = $itemClassName;

        foreach( $items as $item )
        {
            if( ! ( $item instanceof $itemClassName ) )
            {
                throw new InvalidTypeException();
            }

            $this->items[] = $item;
        }
    }

    public function getItemClassName() : string
    {
        return $this->itemClassName;
    }

    public function getItem( int $index )
    {
        if( $index >= $this->count() )
        {
            throw new OutOfRangeException( 'Index: ' . $index );
        }

        return $this->items[ $index ];
    }

    public function indexExists( int $index ) : bool
    {
        if( $index >= $this->count() )
        {
            return false;
        }

        return true;
    }

    //---------------------------------------------------------------------//
    // Implementations                                                     //
    //---------------------------------------------------------------------//

    /**
     * Returns the count of items in the collection.
     * Implements countable.
     * @return integer
     */
    public function count() : int
    {
        return count( $this->items );
    }

    /**
     * Returns an iterator
     * Implements IteratorAggregate
     * @return \ArrayIterator
     */
    public function getIterator()
    {
        return new \ArrayIterator( $this->items );
    }

    public function offsetSet( $offset, $value )
    {
        throw new ImmutabilityException();
    }

    public function offsetUnset( $offset )
    {
        throw new ImmutabilityException();
    }

    /**
     * get an offset's value
     * Implements ArrayAccess
     * @see get
     * @param integer $offset
     * @return mixed
     */
    public function offsetGet( $offset )
    {
        return $this->getItem( $offset );
    }

    /**
     * Determine if offset exists
     * Implements ArrayAccess
     * @see exists
     * @param integer $offset
     * @return boolean
     */
    public function offsetExists( $offset ) : bool
    {
        return $this->indexExists( $offset );
    }
}

这强烈基于 http://aheimlich.dreamhosters.com/generic-collections/Collection.phps 并进行了以下更改:

  1. 基本类型
    • 我删除了所有与基本类型相关的内容。
    • 在我的例子中,所有在模型中、在特定类型的集合中非常重要的东西都非常重要,以至于它们本身应该有一个 ValueObject。
    • 假设我有一个 ID 列表,并且在我的应用程序中使用 sha1 ID。它们对我来说不是字符串。它们是 ID 类型的对象。
  2. Collection 本身是一个值对象,因此它是不可变的。
    • 我阻止了对集合内容的任何编辑权限。
    • 如果这些是基于值对象的集合,那么集合本身就是一个值对象。
    • 所以值是由构造函数传递的,像offsetSet()offsetUnset()这样的方法是被禁止的。

我有一组 4 个基于集合的异常,其中 3 个可能由 Base\Collection 抛出(另一个只是抽象的包罗万象)。

abstract class CollectionException extends \RuntimeException
class ImmutabilityException extends CollectionException
class InvalidTypeException extends CollectionException
class OutOfRangeException extends CollectionException

这些名称非常具有描述性(我希望 ;))。

注意 __construct 有 2 个参数:要存储的对象的类名,以及要放入集合中的通用对象数组。函数的输入类型不能在子类中被覆盖,因此 PHP 中没有任何“好方法”可以做到这一点,除了吃一个泛型数组并通过循环来测试类型。

注意 public function getItem( int $index ) 仍然没有声明返回类型。这是故意的。您可以覆盖输出类型。 “通用”集合在执行时可能会返回“任何东西”。

具体的强类型Dimensions 集合。

现在您有了一个通用类型的 Collection 基础对象(它是抽象的),一个集合需要是一些东西的集合。在您的情况下,是维度对象的集合。

假设有一个 Dimension 命名空间,并且有 Dimension 和Dimensions。

尺寸应该是这样的:

<?php

declare( strict_types = 1 );

namespace XaviMontero\ThrasherPortage\Dimension;

use XaviMontero\ThrasherPortage\Base\Collection\Collection;

class Dimensions extends Collection
{
    public function __construct( array $items = [] )
    {
        parent::__construct( Dimension::class, $items );
    }

    public function getItem( int $index ) : Dimension
    {
        return parent::getItem( $index );
    }
}

在这里你可以看到:

  1. Dimensions 是一个实际上一个集合的类。
  2. 它只实现了 2 个具体方法:构造函数和 getItem。
  3. 这些实现只是对parent包装
  4. __constructor 包装器接受一个东西数组(希望是Dimension 对象),并告诉父级通过硬编码将自己转换为Dimension::class 的集合,Dimensions 不能容纳其他任何东西比Dimension。这样可以确保此类的任何消费者都无法阻止任何事情。
  5. getItem 只是从父项获取项目并强制返回类型为Dimension,因此Dimensions 的任何消费者都严格确定集合返回的任何对象 类型为@ 987654352@.

Picture

你的代码没有变化,除了我喜欢用复数命名集合(例如Dimensions)而不是用一个足够的集合来调用它们(例如DimensionCollection)。

此外,我没有基本的ValueObject 类型,因为它不会给我增加任何东西。即使equals() 在基类中也没有用,因为您不会在抽象基类中强制使用具体类型,并且如果您确实将通用值对象作为equals 的类型,那么您将允许比较苹果和橙子。所以对我来说,值对象只是没有设置器的普通类。

最后,对于强类型的值对象不可变集合,通过getDimensions() 方法给它aawy 是非常安全的,因为没有人能够改变它的内容(请记住基本@ 中的throw new ImmutabilityException(); 987654361@班级)。

Class Picture
{
    /** @var Dimensions */
    private $dimensions;

    public function __construct( Dimensions $dimensions )
    {
        $this->dimensions = $dimensions;
    }

    public function getDimensions() : Dimensions
    {
        return $this->dimensions;
    }
}

消费它

现在您可以通过构建图片并查询它们来使用它了。

我将使用TDD 表示法:expected 来创建值,sut = 被测系统,actual 来查看我得到了什么,所以我可以对此进行单元测试。

设置图片

只需在构造函数中构建所有内容:

$expectedDimension1 = new Dimension( $whateverParamsTakesDimension1 );
$expectedDimension2 = new Dimension( $whateverParamsTakesDimension2 );
$expectedDimension3 = new Dimension( $whateverParamsTakesDimension3 );

$expectedDimensions = new Dimensions( [ $expectedDimension1, $expectedDimension2, $expectedDimension3 ] );

$sut = new Picture( $expectedDimensions );

查询图片

所有类型都完美设置:

$actualDimensions = $sut->getDimensions();           // Forces a Dimensions type.

$actualDimension1 = $actualDimensions->getItem( 0 ); // Forces a Dimension type.
$actualDimension2 = $actualDimensions->getItem( 1 );
$actualDimension3 = $actualDimensions->getItem( 2 );

您现在可以在PictureTest 中断言:

$this->assertSame( $expectedDimensions, $actualDimensions );
$this->assertSame( $expectedDimension1, $actualDimension1 );
$this->assertSame( $expectedDimension2, $actualDimension2 );
$this->assertSame( $expectedDimension3, $actualDimension3 );

纯粹的 TDD

如果你是纯粹主义者,你会

  • 测试DimensionsDimensionsTest中的内容。
  • 不要在Picture中测试Dimensions的内容。
  • 但仍测试PictureTest 中的$actualDimensions
  • 此外,您不会测试assertSame( $a, $b );,但您会assertTrue( $a-&gt;equals( $b ) ); 并拥有equals(),不仅在Dimension 中,而且在基础集合中,它在每个项目上循环并在每个元素上执行equals()

我只是跳过了这部分,因为它超出了问题的范围,这只是关于如何制作集合。

结论

事实上,在公开的方法中,您正在结合您在问题中发布的两种方法。

一方面你使用强类型。

另一方面,您也在循环元素以检查它们的类型。

但是使用接收类型的抽象基集合,并具有强制输入类型(在__construct() 中)和输出类型在getItem() 中的实现,您可以转移单一责任 放入适当的位置(SOLID rulez)。

检查Dimension对象的类型不是图片的责任。那又是谁? Dimensions 应该这样做(通过硬编码传递给基本集合的类型)。然后使用基本集合来制作您作为第一个示例发布的循环,由于语言限制,必须在 PHP 中在运行时完成。

收藏变得简单

现在你有了这个,如果你想扩展你的模型

Picture
Dimension
Dimensions

进入

Picture
Pictures
Dimension
Dimensions

就像创建一个扩展Base\Collection 并将类型强制为PicturePictures 一样简单。就这么简单。

等等。完成。

完整、强类型、完全可测试。还想要什么?

【讨论】:

    【解决方案2】:

    如果您的 Picture VO 拥有一个集合..那么我将其命名为 Pictures 或 PictureCollection,因为它将由其他 Picture 对象组成。 (就像您对尺寸所做的那样)。

    虽然这是一个审查事项,但 valueObject 接口应命名为“ValueObjetct”,大写“V”。

    我认为您的域需要进行一些重组。如果图片有Dimensions than Dimension"s"应该是一组Dimension VOs。

    【讨论】:

    • @keihm,为什么需要ValueObject 接口?
    • @AboElzooz 我可以看到您正在使用一个接口,我假设该接口包含诸如 toString() 和 equals() 之类的方法。我个人不使用它们,因为我发现它们更多地是关于代码而不是域,但我确实曾经使用它们来创建关于如何构建 VO 的规则,然后一旦它印在我的脑海中,我就停止了:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多