Reputation: 319
i have a simple question when building a value object that has a collection of value objects inside it with a specific type how do you construct the object ?
to take an example lets say you have a picture
that take multiple dimensions
Option 1 :
Class Picture implements valueObject{
public function __construct(array $dimensions){
foreach($dimensions as $dimension){
// check if instance of `dimension` value object
}
}
}
Option 2 :
Class Picture implements valueObject{
public function __construct(DimensionCollection $dimensions){
}
}
Class DimensionCollection implements Traversable{
public function add(Dimension $dimension){
// add to array
}
}
Option two off-course seems more logical but is there another pattern that is better taken this from DDD preceptive ?
Upvotes: 4
Views: 3988
Reputation: 10725
This first hing you have to do is list all the concepts that come to your mind when analyzing the domain. Here I see:
Picture
Dimension
Dimensions
Even, maybe behind the Dimensions
object probably you have a generic (abstract) value-object Collection
concept that can feed Dimensions
as well as many other strongly-typed collections.
Even you could have an Interfaces\Collection
to allow any element in your domain to accept "generic collections of things" if you are doing it quick and don't want to string-type something until you refine your model.
Provided PHP does not have class templates, like in C++ and you can't do something like class Dimensions extends Collection<Dimension>
what I do is to force the type by strictily checking the elements at input.
Therefore my collections are conscious of what is the class they hold, and the interface reflects that via the getItemClassName()
method.
Collection
interfaceHere is what my basic Interfaces\Collection
looks like:
<?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 );
}
NOTE that the getItem() does not declare an explicit return type. I will override the return type in the classes implementing the interface.
Collection
Here is what my basic abstract Collection
(which will be implemented by other type-specific classes) looks like:
<?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 );
}
}
This is strongly based on http://aheimlich.dreamhosters.com/generic-collections/Collection.phps with the following changes:
Collection
itself is a value-object, so it is immutable.
offsetSet()
and offsetUnset()
are forbidden.I have a set of 4 collection-based exceptions, 3 of which may be thrown by the Base\Collection
(the other is just an abstract catch-all).
abstract class CollectionException extends \RuntimeException
class ImmutabilityException extends CollectionException
class InvalidTypeException extends CollectionException
class OutOfRangeException extends CollectionException
The names are pretty descriptive (I hope ;) ).
NOTE that the __construct
takes 2 parameters: The classname of the objects that would be stored, and a generic array of objects to put inside the collection. An input type of a function cannot be overriden in subclasses, so there is not any "nice way" in PHP to do this other than eating a generic array and testing the type by looping over it.
NOTE that still the public function getItem( int $index )
does not declare a return type. This is done on purpose. You may override the output type. The "generic" collection may return "anything" at this point of implementation.
Dimensions
collection.Now that you have a generic typed Collection base object (which is abstract) a collection needs to be a collection of something. In your case a collection of dimension objects.
Let's assume there is a Dimension namespace and there you have Dimension and Dimensions.
Here's what Dimensions should look like:
<?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 );
}
}
Here you can see that:
parent
.__constructor
wrapper takes an array of things (hopefully of Dimension
objects) and tells the parent to convert itself to a collection of Dimension::class
by hardcoding that the Dimensions
can't hold any other thing other than Dimension
. This ensures no consumer of this class can brak anything.Dimension
so any consumer of Dimensions
is strictly sure that any object returned by the collection is of type Dimension
.Picture
classNo changes from your code, except that I like to name collections with a plural (as for instance like in Dimensions
) instead of calling them with a collection suffic (as for instance like in DimensionCollection
).
In addition I don't have a basic ValueObject
type, as it does not add anything to me. Even the equals()
is not useful in a base class because you do not force a concrete type in the abstract base class, and if you do place a generic value object as the type of the equals
, then you would be allowing to compare apples with oranges. So for me value objects are just plain classes with no setters.
Finally, with a strongly-typed value-object immutable collection, it is very safe to give it aawy via a getDimensions()
method, because nobody will be able to alter its contents (remember the throw new ImmutabilityException();
in the basic Collection
class).
Class Picture
{
/** @var Dimensions */
private $dimensions;
public function __construct( Dimensions $dimensions )
{
$this->dimensions = $dimensions;
}
public function getDimensions() : Dimensions
{
return $this->dimensions;
}
}
Now you are ready to consume it by building pictures and querying them.
I will use TDD
notation: expected
to create the values, sut
= system under test, and actual
to see what I got, so I can unit-test this.
Just build everything in the constructors:
$expectedDimension1 = new Dimension( $whateverParamsTakesDimension1 );
$expectedDimension2 = new Dimension( $whateverParamsTakesDimension2 );
$expectedDimension3 = new Dimension( $whateverParamsTakesDimension3 );
$expectedDimensions = new Dimensions( [ $expectedDimension1, $expectedDimension2, $expectedDimension3 ] );
$sut = new Picture( $expectedDimensions );
All the types are perfectly set:
$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 );
If you are purist you would
Dimensions
in DimensionsTest
.Dimensions
in Picture
.$actualDimensions
in PictureTest
.assertSame( $a, $b );
but you would assertTrue( $a->equals( $b ) );
and have an equals()
not only in Dimension
but also in the base collection which loops on each item and does an equals()
on each element.I just skipped this part as it is out of the scope of the question, which was only on how to make the collection.
In fact, in the method exposed you are combining the two methods you post in your question.
On one side you are using strong-types.
On the other you are also looping on the elements to check their type.
But using an abstract base-collection which receives the type, and having implementations that force the input types (in the __construct()
) and the output types in the getItem()
you are allowed to shift the single responsability into the proper place (SOLID rulez).
It is not a responsability of the picture to check the types of the Dimension
objects. Who then? It is the Dimensions
who should do it (via hardcoding the type passed to the basic collection). And then have the basic collection to make the loop that you post as the first example, which, by language limitations, must be done at runtime in PHP.
Now that you have this, if you want to expand your model from
Picture
Dimension
Dimensions
into
Picture
Pictures
Dimension
Dimensions
it is as easy as creating a Pictures
that extends the Base\Collection
and forces the type to Picture
. As easy as that.
Et voilà. Done.
Complete, strongly typed, fully testable. What else would one want?
Upvotes: 11
Reputation: 1618
If your Picture VO holds a collection..then I'd name it Pictures or PictureCollection because it will be made of of other Picture objects. (as you did with dimensions).
Although this is a review matter valueObject interface should be named "ValueObjetct" with a capital "V".
I think your domain needs a bit of restructuring. If a Picture has Dimensions than Dimension"s" should be a group made of Dimension VOs.
Upvotes: 1