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