Every day design pattern: Builder
Just like the factory pattern, the builder is a creational pattern, meaning it is about how objects are created. But unlike a factory, a builder allows you to build an object in parts. I tend to use it for creating objects that take a configuration.
Lets start with an example. This class builds a guzzle client, with a certain config. Normally we have a timeout of 10 seconds, with a 5 second timeout for both connect and read.
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
final class GuzzleClientBuilder
{
private array $config = [
RequestOptions::TIMEOUT => 10,
'defaults' => [
RequestOptions::CONNECT_TIMEOUT => 5,
RequestOptions::READ_TIMEOUT => 5,
],
];
public static function defaultClient(): Client
{
return self::init()->build();
}
public static function init(): self
{
return new self();
}
public function withTimeout(int $timeout): self
{
$this->config[RequestOptions::TIMEOUT] = $timeout;
return $this;
}
public function withConnectTimeout(int $timeout): self
{
$this->config['defaults'][RequestOptions::CONNECT_TIMEOUT] = $timeout;
return $this;
}
public function withReadTimeout(int $timeout): self
{
$this->config['defaults'][RequestOptions::READ_TIMEOUT] = $timeout;
return $this;
}
public function build(): Client
{
return new Client($this->config);
}
}
So if we want to use this builder to create a client with a 30 second timeout, we would do the following:
$guzzle = GuzzleClientBuilder::init()->withTimeout(30)->build();
By using the builder we still have all the other config, so the timeout for connection would still be 5 seconds.
Another example you may have used is a Query Builder
. For example the Doctrine Query builder.
The query builder, as the name suggests, allows you to build a query. One of the strengths of this is that you can have conditionals, like so:
function getUsers(EntityManager $em, bool $onlyActive): array
{
$qb = $em->createQueryBuilder();
$qb->select('u')
->from('User', 'u')
if($onlyActive) {
$qb->where('active = 1')
}
return $qb->getQuery()->getArrayResult();
}
Now at runtime you can determine if you want only the active users, or all users. And you don’t need to do any magic like concat strings together so the sql is valid.
Why use it
A strength of the builder pattern is that it allows you to build up an object bit by bit. But, because you do this before you actually create the object, the end result can be immutable. Meaning you wont need setters on the object. Immutable objects are generally easier to debug and reason about.
The builder object itself is mutable by nature, its sole intention is to be dynamically changed. But, the builder object is usually shot lived, while the resulting object may be used in a lot of places.
In conclusion
The builder pattern is a creational pattern, that allows you to build an object dynamically. It can help in making objects immutable. It can also help make the configuration of an object easier to understand, as it can be done in steps.