AboElzooz
AboElzooz

Reputation: 319

DDD building a value object that holds a collection property

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

Answers (2)

Xavi Montero
Xavi Montero

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.

The Collection interface

Here 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.

The basic abstract immutable 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:

  1. Basic types
    • I removed all the things related to basic types.
    • In my case, all things that are so important to be in the model, in a type-specific collection, are so important that they deserve a ValueObject by themselves.
    • Imagine I have a list of IDs and in my application I use sha1 IDs. They are not strings for me. They are objects of type ID.
  2. The Collection itself is a value-object, so it is immutable.
    • I blocked any access to editing the content of the collection.
    • If those are value-object based collections, the collection itself is a value object itself.
    • So values are passed by constructor, and methods like 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.

The concrete strongly-typed 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:

  1. Dimensions is a class that in fact is a Collection.
  2. It only implements 2 concrete methods: The constructor and the getItem.
  3. The implementations are mere wrappers to the parent.
  4. The __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.
  5. The getItem just gets the item from the parent and forces a return type of type Dimension so any consumer of Dimensions is strictly sure that any object returned by the collection is of type Dimension.

The Picture class

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

Consuming it

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.

Setup the picture

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

Query the picture

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

You can now assert in PictureTest:

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

Purist TDD

If you are purist you would

  • Test the content of Dimensions in DimensionsTest.
  • Do not test the contents of Dimensions in Picture.
  • But still test the $actualDimensions in PictureTest.
  • Also you would not test 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.

CONCLUSION

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.

Collections made easy

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

Keith Mifsud
Keith Mifsud

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

Related Questions