The Other Builder Pattern
First, let me just briefly mention that there's a different pattern under the same name: Joshua Bloch's Builder pattern described in Effective Java (see the excerpt here). This pattern was inspired by the GoF Builder pattern; however, it's purpose/intent is not the same - it solves a different problem, so, despite what that article says, it is a separate pattern. Basically, Bloch Builder was designed to help create an object with an unwieldy constructor; it's essentially a factory with a fluent interface. In the article, Joshua Bloch describes it as a way to simulate optional named parameters, but it can also be seen as way to provide a small domain-specific language.
GoF Builder
What is it for?
The GoF book says that the intent is to "separate the construction of a complex object from its representation so that the same construction process can create different representations." Looking at this in the context of an evolving codebase, the assumption is that there's some (probably nontrivial) construction logic that needs to produce several structurally quite different representations, and that these two (construction logic, and the kinds of products) need to evolve independently. Nevertheless, there is some way to describe the construction process in a generalized manner (again, this is a starting assumption that justifies the application of the pattern).
To decouple the two, an abstraction is introduced: the Builder interface (or an abstract class). It captures the essence of this general way of thinking about the construction process, across all the different kinds of products. Essentially, it's an application of the Dependency Inversion Principle:
Component A Component B
[Director]-------->[Product]
becomes
/------- Component A ------\ /------- Component B --------\
[Director]-------->[Builder]<|---------[ConcreteBuilder]---->[Product]
For this to work, the Builder interface needs to remain relatively stable as the code evolves.
Now, I'm not sure how often GoF Builder comes up in practice; Bloch Builder seems to be used more. In any case, GoF Builder is worth knowing, and in the context of teaching, it can be used to discuss abstraction.
We need to go a bit more concrete
I'm going to make your example slightly more concrete in order to be able to better discuss certain points. It's going remain quite contrived, though. I don't normally code in PHP, so if something looks off, feel free to fix it. My changes here are geared towards demonstrating the points I want to make, but you may need to simplify/alter the code for teaching purposes - I'll leave that decision to you.
In my version, the product is an "animal farm". Let's say that you need to construct different representations of it from a string of this form (I'll refer to this as the "farm string"):
"Dogs: Fido, Apollo, Molly, Hunter, Daisy; Cows: Daisy, Rosie, Thor; Cats: Leo, Nala, Roxy"
This is just so that there's some nontrivial construction logic involved, to better motivate having the Director class. The string itself may come from a database (or some other source). This is a custom format, but in a more realistic scenario, it may be in some standard format, like JSON.
Essentially, the Director needs to interpret this string in order to reconstitute an object into some representation of an "animal farm". But it needs to do it in a generalized way - the construction algorithm should be independent of the structure of the product.
(BTW, this farm only supports Cows, Dogs and Cats. This is on purpose; these represent different "parts" of the conceptual product (the "farm"). The ability to support arbitrary kinds of animals is not the focus or the point of this discussion. Rather, the focus is on how to separate the generalized construction process from the implementation details of each product).
Now, suppose that you're developing this system, and that you've come up with the builder interface shown below. Its design reflects your understanding of the construction problem, and of the kinds of changes that are most/least likely (to be requested by clients) - perhaps you've settled on this interface after several iterations. So, the assumption is that this is general enough to support the most likely kind of future changes. I.e., it's unlikely that the interface itself will have to be changed; instead the requirements can be met by changing the ConcreteBuilder-s and/or the Director.
interface FarmBuilder {
public function makeCow(string $name);
public function makeDog(string $name);
public function makeCat(string $name);
}
The Director will interpret the input "farm string" (it just splits it using various delimiters), and construct a product through the FarmBuilder interface in the following way:
class Director {
private string $farmString;
function __construct(string $farmString) {
$this->farmString = $farmString;
}
public function createAnimalFarm(FarmBuilder &$builder) {
$groups = explode(";", $this->farmString);
foreach ($groups as $groupString) {
// Identify group (Cows, Dogs, Cats)
$temp = explode(":", $groupString);
$groupKey = strtolower(trim($temp[0]));
$groupAnimalsString = $temp[1];
// Get the individual animals in the group
$groupAnimals = explode(",", $groupAnimalsString);
$groupAnimals = array_map(function($str) { return trim($str); }, $groupAnimals);
switch ($groupKey) {
case "cows":
foreach ($groupAnimals as $animalName) $builder->makeCow($animalName);
break;
case "dogs":
foreach ($groupAnimals as $animalName) $builder->makeDog($animalName);
break;
case "cats":
foreach ($groupAnimals as $animalName) $builder->makeCat($animalName);
break;
}
}
}
}
So, the Director encapsulates some construction algorithm. Note that there could be other Director classes that a programmer can choose from; e.g., maybe there's one that uses JSON input, or a stream of binary data. Some of the Director-s could, in principle, be hardcoded as in your original code, that is not forbidden. But, if the requirement to keep the construction process decoupled from the products isn't really there to justify the complexity introduced by the pattern, then it's probably better to just provide a couple of static factory methods on the products themselves, that provide a way to create predefined instances.
"I've found some Java examples where the Director receives builder by parameter without any kind of return. On the other hand, I've also seen examples using the Director's constructor to receive a builder object"
Here, I've chosen to pass the builder as parameter to the createAnimalFarm method, because I wanted to be able to reuse the director with different concrete builders. Passing it via the constructor is another option. It would still be the same pattern - there's quite a bit of wiggle room when it comes to the exact implementation; what matters is the roles of the different elements and the overall relationships between them, and how it all works together to support the intent of the pattern. (This is why we have the notion of a concrete realization of a pattern.) The fact that I've chosen to pass the "farm string" via the constructor is also not relevant in terms of how the pattern is defined.
That is to say, design patterns don't prescribe every detail; there is no "one true way" to implement them.
The Products
The products usually have a complex structure (but they don't have to), in the sense that they may be somehow composed of other objects. The products can be very different, and serve different purposes, and so are likely to have dissimilar interfaces.
Let's say that these are the products that need to be built:
Product 1: An Instance of the Farm Class
class Animal {
public string $species;
public string $name;
function __construct($species, $name) {
$this->species = $species;
$this->name = $name;
}
}
class Farm {
private $animals = []; // elements are Animal instances
function __construct(array $animals) {
foreach ($animals as $animal) {
$this->animals[] = $animal;
}
}
public function getRandomAnimal() : Animal {
$count = count($this->animals);
return $this->animals[rand(0, $count - 1)];
}
public function petRandomAnimal() : string {
// Produces a string of the form:
// "We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous."
// (omitted...)
}
}
This is rather contrived, but the idea is to demonstrate that construction and/or modification interface of the product doesn't have to offer the same level of granularity as that of the Builder interface. Here, the Farm class is immutable, with a one-off constructor; however, the builder abstraction provides an incremental construction interface. The builder for this product is shown below:
class DefaultFarmBuilder implements FarmBuilder {
private $animals = [];
public function makeCow(string $name) {
$this->animals[] = new Animal("Cow", $name);
}
public function makeDog(string $name) {
$this->animals[] = new Animal("Dog", $name);
}
public function makeCat(string $name) {
$this->animals[] = new Animal("Cat", $name);
}
public function getFarm(): Farm {
return new Farm($this->animals);
}
}
This particular builder essentially incrementally builds the parameter for the Farm constructor, and only constructs the farm object when the getFarm method is called (the "getProduct" method for this builder). This builder, as implemented, always returns a new instance, but the implementation can be changed to return the same instance every time, if that makes more sense.
Now, if client code needs to create and somehow manipulate a Farm object, it can do it like this:
// Construct a Farm using the algorithm represented by the Director class
$farmBuilder = new DefaultFarmBuilder();
$director->createAnimalFarm($farmBuilder);
$farm = $farmBuilder->getFarm();
// Use the farm object
echo $farm->petRandomAnimal();
// Output:
// We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous.
Product 2: An array of AnimalOwner instances
class AnimalOwner {
private string $name;
private $animals = [];
function __construct(string $name) {
$this->name = $name;
}
public function addAnimal(string $species, string $name) {
$this->animals[] = $name . " (" . $species . ")";
}
public function toString() : string {
// Produces a string of the form:
// "[Owner] owns [animals]."
// (omitted...)
}
}
Unlike the previous example, this class provides a way to add individual animals, and its builder can make use of that. However, to construct an array of these, processing of some special-purpose data is required (owner names, and ownership relationships); the idea is that this is specific to this usage (the other types of product don't need ownership data), so the abstract builder interface doesn't provide any support. This will be handled by the concrete builder:
class OwnerArrayBuilder implements FarmBuilder {
private $ownershipMap = [];
private $owners = [];
private int $animalIndex = -1;
function __construct(array $ownershipData) {
// $ownershipData example:
// Given: "Dogs: Fido, Molly; Cats: Nala" (F, M, N)
// $ownershipData value of F M N
// ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 1, 0]]
// means that Fido and Nala belong to Alice (the owner at index 0),
// and that Molly belongs to Bob (the owner at index 1)
$owners = $ownershipData["Owners"];
foreach($owners as $owner) {
$this->owners[] = new AnimalOwner($owner);
}
$this->ownershipMap = $ownershipData["OwnershipMap"];
}
public function makeCow(string $name) {
$this->animalIndex++;
$ownerIndex = $this->ownershipMap[$this->animalIndex];
$this->owners[$ownerIndex]->addAnimal("Cow", $name);
}
public function makeDog(string $name) {
$this->animalIndex++;
$ownerIndex = $this->ownershipMap[$this->animalIndex];
$this->owners[$ownerIndex]->addAnimal("Dog", $name);
}
public function makeCat(string $name) {
$this->animalIndex++;
$ownerIndex = $this->ownershipMap[$this->animalIndex];
$this->owners[$ownerIndex]->addAnimal("Cat", $name);
}
public function getOwners(): array {
return $this->owners;
}
}
So, here, the builder is provided with additional data (perhaps it came from the same database as the "farm string"). In this case, the resulting array and the AnimalOwner objects are created immediately, and each of the builder functions (makeCow, makeDog, makeCat) calls the addAnimal() function on those instances. The "getProduct" function, here called getOwners, returns a completely different type; this is why it isn't a part of the abstract builder interface - the products aren't unified. Client code creates the concrete builder anyway, so it can use it to retrieve the product:
// Ownership data imported from somewhere:
$ownershipData = ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1]];
// Construct the AnimalOwner array using the algorithm represented by the Director class
$ownerArrayBuilder = new OwnerArrayBuilder($ownershipData);
$director->createAnimalFarm($ownerArrayBuilder);
$owners = $ownerArrayBuilder->getOwners();
// use the owners array:
$ownerStrings = array_map(function($owner) {return $owner->toString();}, $owners);
echo join("\n", $ownerStrings);
// Output:
// Alice owns Fido (Dog), Apollo (Dog), Molly (Dog), Hunter (Dog), Daisy (Cow) and Leo (Cat).
// Bob owns Daisy (Dog), Rosie (Cow), Thor (Cow), Nala (Cat) and Roxy (Cat).
Product 3: An HTML Table that Summarizes the Animals on the Farm
This product is not a custom, user-defined class. Instead, what needs to be created is a DOMDocument that represents an HTML table of the following form:
+------+----------+
| Cows | #numCows |
+------+----------+
| Dogs | #numDogs |
+------+----------+
| Cats | #numCats |
+------+----------+
In this case, the builder essentially provides a special-purpose construction mechanism over the general DOMDocument/DOMElement APIs:
class HtmlSummaryTableBuilder implements FarmBuilder {
private DOMDocument $dom;
private $countCells = [];
private $groups = ['Cows', 'Dogs', 'Cats'];
function __construct() {
$this->dom = new DOMDocument();
$table = $this->dom->createElement('table');
foreach ($this->groups as $group) {
$tableRow = $this->dom->createElement('tr');
$tableCell_Group = $this->dom->createElement('td', $group);
$tableCell_Count = $this->dom->createElement('td');
$this->countCells[$group] = ["Cell" => $tableCell_Count, "Count" => 0];
$tableRow->appendChild($tableCell_Group);
$tableRow->appendChild($tableCell_Count);
$table->appendChild($tableRow);
}
$this->dom->appendChild($table);
}
public function makeCow(string $name) {
$this->countCells["Cows"]["Count"]++;
}
public function makeDog(string $name) {
$this->countCells["Dogs"]["Count"]++;
}
public function makeCat(string $name) {
$this->countCells["Cats"]["Count"]++;
}
public function getTable(): DOMDocument {
foreach ($this->groups as $group) {
$this->countCells[$group]["Cell"]->appendChild(
$this->dom->createTextNode(strval($this->countCells[$group]["Count"])));
}
return $this->dom;
}
}
Aside: Given that the table only contains the count, the builder ignores animal names, even though it receives them. The idea is that the designer(s) had reason to include the support for the name in the builder interface. It seems that the names are used by most product types, and perhaps our hypothetical developers anticipated, based on their experience with the project so far, that future changes will generally make use of the name as well.
Client code can then create the DOMDocument like this:
$tableBuilder = new HtmlSummaryTableBuilder();
$director->createAnimalFarm($tableBuilder);
// obtain and use the table
echo $tableBuilder->getTable()->saveHTML();
// Output:
<table>
<tr><td>Cows</td><td>3</td></tr>
<tr><td>Dogs</td><td>5</td></tr>
<tr><td>Cats</td><td>3</td></tr>
</table>
Summary
Hopefully, this served to show that concrete builders don't have to all follow the same form; they can do very different things. The key abstraction here is the generalized construction mechanism. The Director and the concrete Builders depend on it, which makes them decoupled - as long as this interface is expressive enough to support the construction of various products, while itself being stable in the face of change. In general, the different concrete builders can come from different modules, and can be developed independently (by different individuals/teams). Both the construction algorithm and the product builder can vary independently. Different Director-s can be defined, as long as they too can make use of the abstract builder interface. E.g., new Director-s could be added in order to support a better construction algorithm, a different input format, or to provide predefined "preset" objects.
P.S. Here's the entire example in a PHP Sandbox.
$logger = new LoggerBuilder()->withDateTime()->withLogLevel()->withHtml()
to generate a logger which will log files, append dateTime when the log happend, then append LogLevel (Warn, Debug, Information, Tracing etc) and format it with Html (i.e. if its meant to be output on a website or email). What you have is more of a factory pattern, not build pattern.Build()
or similar.