Skip to content

Protecting invariants when creating value objects through Entity or Factory #34

@webdevilopers

Description

@webdevilopers

My original post and poll started here:
https://twitter.com/webdevilopers/status/1100102866583339008

This was the original gist:

Use case:
I need to calculate a Dormer based on specific DormerType (Entity).
Business rule: Some DormerTypes can not have a Gutter attached.
Dormer in the end will just be a value object stored inside an DormerCalculation Entity. / Aggregate Root that could throw the DormerCalculated Domain Event.
This means that the DormerTypeId should only be a reference.

I currently think of three approaches.

Version 1:

The full Entity is passed to the value object. This feels ugly. Allthough it is now impossible for a developer to create an inconsistent object. E.g. by attaching a gutter to a dormer with a type that does not allow gutters.

<?php

final class Dormer
{
    /** @var DormerType $type */
    private $type;

    public static function construct(DormerType $type)
    {
        $this->type = $type;
    }

    public function addGutter(Gutter $gutter)
    {
        if (!$this->type->canHaveGutterInstalled()) {
            throw new DormerTypeCannotHaveGutterInstalledException();
        }
 
       $this->gutter = $gutter;
    }
}
final class DormerCalculationFactory extends AbstractCalculator
{
    private $dormerTypeRepository;

    public function createWithDto(CalculateDormerCommand $dto)
    {
        $dormerType = $this->dormerTypeRepository->ofId(DormerTypeId::fromInteger($dto->dormerTypeId));

        /** @var Dormer $dormer */
        $dormer = Dormer::construct($dormerType);
        $dormer->addGutter(Gutter::fromDto($dto->gutter));
    }
}

Version 2:

Again the business rule is ensured. The value object is created through the dormer type Entity which sounds logical. But in the end it is not an Aggregate Root that e.g. could throw a "DormerCalculated" event.
In addition the factory method calculateDormer and the factory method on the dormer value object would look too identical.

<?php

class DormerType()
{
    private $id;

    public function calculateDormer(Gutter $gutter)
    {
         if (!$this->canHaveGutterInstalled()) {
            throw new DormerTypeCannotHaveGutterInstalledException();
        }
       
        return new Dormer($this->id(), $gutter);
    }
}

Version 3:

All creation logic is inside the factory. It protects the invariants and creates the Dormer only with the DormerTypeId.
But a developer could create an inconsistend value object by skipping the factory. Ouch!?

final class DormerCalculationFactory extends AbstractCalculator
{
    private $dormerTypeRepository;

    public function createWithDto(CalculateDormerCommand $dto)
    {
        $dormerType = $this->dormerTypeRepository->ofId(DormerTypeId::fromInteger($dto->dormerTypeId));

        /** @var Dormer $dormer */
        $dormer = Dormer::construct($dormerType->id());

        if (null !== $dto->gutter && $dormerType->canHaveGutterInstalled()) {
            $dormer->addGutter(Gutter::fromDto($dto->gutter));
        }
    }
}

After reading a post on @culttt b @philipbrown version 3 still feels like the best way though.

Due to the coupled nature of the Factory and the object, it is usually fine to allow the Factory to protect the invariants of the object.

What do you guys think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions