DDD строит объект значения, который содержит свойство коллекции

У меня есть простой вопрос при создании объекта значения, в котором есть коллекция объектов значения определенного типа, как вы строите объект?

взять пример, скажем, у вас есть 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?

2

Решение

Первое, что вам нужно сделать, это перечислить все концепции, которые приходят вам на ум при анализе домена. Здесь я вижу:

Picture
Dimension
Dimensions

Даже, может быть, за Dimensions объект, вероятно, у вас есть общий (абстрактный) значение-объект Collection концепция, которая может накормить Dimensions а также многие другие строго типизированные коллекции.

Даже вы могли бы иметь Interfaces\Collection разрешить любому элементу в вашем домене принимать «общие наборы вещей», если вы делаете это быстро и не хотите что-то вводить в строку, пока не доработаете свою модель.

При условии, что PHP не имеет шаблонов классов, как в C ++, и вы не можете делать что-то вроде class Dimensions extends Collection<Dimension> что я делаю, так это форсирую тип, строго проверяя элементы на входе.

Поэтому мои коллекции осознают, какой класс они держат, и интерфейс отражает это через getItemClassName() метод.

Вот то, что мой основной 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 (который будет реализован другими классами, зависящими от типа) выглядит следующим образом:

<?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.
    • Представьте, что у меня есть список идентификаторов, и в своем приложении я использую идентификаторы sha1. Они не струны для меня. Это объекты типа 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 ) не объявляет тип возвращаемого значения. Это сделано специально. Вы может переопределить тип вывода. «Универсальная» коллекция может возвращать «что угодно» в этой точке реализации.

Теперь, когда у вас есть базовый типизированный базовый объект 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. Размеры это класс, который на самом деле является Коллекция.
  2. Он реализует только 2 конкретных метода: конструктор и getItem.
  3. Реализации являются простые обертки к parent,
  4. __constructor Оболочка принимает множество вещей (надеюсь, Dimension объекты) и говорит родителю преобразовать себя в коллекцию Dimension::class путем жесткого кодирования, что Dimensions не может держать ничего, кроме Dimension, Это гарантирует, что ни один потребитель этого класса не сможет ничего сломить.
  5. GetItem просто получает элемент от родителя и вызывает тип возвращаемого типа Dimension поэтому любой потребитель Dimensions строго уверен, что любой объект, возвращаемый коллекцией является типа Dimension,

Никаких изменений в вашем коде, за исключением того, что мне нравится называть коллекции множественным числом (как, например, как в Dimensions) вместо того, чтобы вызывать их с достаточной коллекцией (как, например, как в DimensionCollection ).

Кроме того, у меня нет основного ValueObject типа, так как это ничего не добавляет для меня. Даже equals() бесполезен в базовом классе, потому что вы не навязываете конкретный тип в абстрактном базовом классе, и если вы помещаете объект универсального значения в качестве типа equalsТогда вы позволите сравнить яблоки с апельсинами. Поэтому для меня объекты-значения — это просто классы без сеттеров.

Наконец, со строго типизированной неизменяемой коллекцией объекта-значения очень безопасно дать ей предупреждение через getDimensions() метод, потому что никто не сможет изменить его содержимое (помните throw new ImmutabilityException(); в основном Collection учебный класс).

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

Если вы пурист, вы бы

  • Проверьте содержание Dimensions в DimensionsTest,
  • Не проверяйте содержимое Dimensions в Picture,
  • Но все же проверить $actualDimensions в PictureTest,
  • Также вы бы не тестировали assertSame( $a, $b ); но вы бы assertTrue( $a->equals( $b ) ); и есть equals() не только в Dimension но и в базовой коллекции, которая зацикливается на каждом элементе и делает equals() на каждом элементе.

Я просто пропустил эту часть, поскольку она выходит за рамки вопроса, который был только о том, как сделать коллекцию.

Фактически, в раскрытом методе вы объединяете два метода, которые вы публикуете в своем вопросе.

С одной стороны вы используете сильные типы.

С другой стороны, вы также зацикливаетесь на элементах, чтобы проверить их тип.

Но используя абстрактную базовую коллекцию, которая получает тип, и имея реализации, которые форсируют входные типы (в __construct()) и типы вывода в getItem() вам разрешено сдвинуть одиночная ответственность в нужное место (SOLID рулез).

Это не обязанность картины проверять типы Dimension объекты. Кто тогда? Это Dimensions кто должен это делать (посредством жесткого кодирования типа, передаваемого в базовую коллекцию). А затем создайте базовую коллекцию для создания цикла, который вы публикуете в качестве первого примера, что, в зависимости от языковых ограничений, должно выполняться во время выполнения в PHP.

Теперь, когда у вас есть это, если вы хотите расширить свою модель с

Picture
Dimension
Dimensions

в

Picture
Pictures
Dimension
Dimensions

это так же просто, как создать Pictures что расширяет Base\Collection и заставляет тип Picture, Это так просто.

И вуаля. Готово.

Полный, строго типизированный, полностью тестируемый. Что еще хотелось бы?

4

Другие решения

Если ваш Picture VO содержит коллекцию … тогда я назову его Pictures или PictureCollection, потому что он будет сделан из других объектов Picture. (как вы сделали с размерами).

Хотя это вопрос обзора, интерфейс valueObject должен называться «ValueObjetct» с большой буквы «V».

Я думаю, что ваш домен нуждается в небольшой реструктуризации. Если картинка имеет размеры, то размерность «s» должна быть группой, составленной из VO измерений.

0

По вопросам рекламы [email protected]