Search code examples
phpdesign-patternsdomain-driven-designbuilderbuilder-pattern

Should a builder accept primitives or value objects


Given an Address at a minimum must have a $firstLine and $postcode but can contain optional properties, I am looking to implement a builder to ease the construction of an Address.

An abridged Address might look like:

class Address
{
    /**
     * @var AddressLine
     */
    private $firstLine;

    /**
     * @var null|AddressLine
     */
    private $secondLine;

    /**
     * Other properties excluded for brevity
     */
    ...

    /**
     * @var Postcode
     */
    private $postcode;

    /**
     * @param AddressLine $firstLine
     * @param null|AddressLine $secondLine
     * ...
     * @param Postcode $postcode
     */
    public function __construct(AddressLine $firstLine, AddressLine $secondLine, ... , Postcode $postcode)
    {
        $this->firstLine = $firstLine;
        $this->secondLine = $secondLine;
        ...
        $this->postcode = $postcode;
    }

    public static function fromBuilder(AddressBuilder $builder)
    {
        return new self(
            $builder->firstLine(),
            $builder->secondLine(),
            ... ,
            $builder->postcode()
        );
    }
}

The above seems to make sense for me, a public constructor which protects its invariants through typehints and allowing traditional construction, additionally a factory method which accepts an AddressBuilder that might look something like the following:

class AddressBuilder
{
    public function __construct(...)
    {
    }

    public function withSecondLine(...)
    {
    }

    public function build()
    {
        return Address::fromBuilder($this);
    }
}

In regards to the AddressBuilder, should it accept primatives which are validated during the build() method, or should it expect the relevant Value Object?

With Primitives

public function withSecondLine(string $line)
{
    $this->secondLine = $line;
}

public function build()
{
    ...
    $secondLine = new AddressLine($this->secondLine);

    return new Address(... , $secondLine, ...);
}

With Value Objects

public function withSecondLine(AddressLine $secondLine)
{
    $this->secondLine = $secondLine;
}

public function build()
{
    return Address::fromBuilder($this);
}

Solution

  • In regards to the AddressBuilder, should it accept primitives which are validated during the build() method, or should it expect the relevant Value Object?

    Either approach is fine.

    Working with primitives tends to be best when you are at the boundary of your application. For example, when you are reading data from the payload of an http request, an API expressed in domain agnostic primitives is probably going to be easier to work with than an API expressed in domain types.

    As you get closer to the core of the application, working in the domain language makes more sense, so your API is likely to reflect that.

    One way to think about it is that that builder pattern is largely an implementation detail. The consumer, in simple cases, is just a function

    BowlingGame buildMeABowlingGameForGreatGood(int.... rolls) {
        BowlingGame.Builder builder = ...
        rolls.forEach(r -> {
            builder.addRoll(r)
        } )
        return builder.build();
    }
    

    and the consumers of the function don't care at all about the details.

    You might even have different builder APIs, so that different client contexts can call the most suitable one

    BowlingGame buildMeABowlingGameForGreatGood(int.... rolls) {
        BowlingGame.PrimitiveBuilder primitiveBuilder = new PrimitiveBuilder(
            new BowlingGame.ModelBuilder(...)
        );
    
        // ...
    }
    

    Where things have the potential to get interesting is if you aren't sure that the arguments are going to pass the validation checks.

    AddressBuilder builder = ...
    
    // Do you want to reject an invalid X here?
    builder.withSecondLine(X)
    
    // Or do you prefer to reject an invalid X here?
    builder.build()
    

    The builder pattern gives you a handle to the mutable state of the build in progress, which you can pass around. So the build statement may be arbitrarily far away from the withSecondLine statement. If you already know that X is valid (because it's a model value object already), then it probably doesn't matter much. If X is a primitive, then you might care quite a bit.