Login to bookmark this video
Buy Access to Course
04.

Builder Pattern

|

Share this awesome video!

|

Time for "design pattern" number two: the "builder pattern". This is one of those creational patterns that help you instantiate and configure objects. And, it's a bit easier to understand than the "strategy pattern".

Official Definition

The official definition of the "builder pattern" is this:

A creational design pattern that lets you build and configure complex objects step-by-step.

That... actually made sense. Part two of the official definition says:

the pattern allows you to produce different types and representations of an object using the same construction code.

In other words, you create a builder class that helps build other objects... and those object might be of different classes or the same class with different data.

Simple Example

A simple example might be a pizza parlor that needs to create a bunch of pizzas, each with a different crust, toppings, etc. To make life easier, the owner of the pizza parlor, who's a Symfony developer by night, decides to create a PizzaBuilder class with easy methods like addIngredient(), setDough(), and addCheese(). Then, they create a buildPizza() method, which takes all of that info and does the heavy lifting of creating that Pizza object and returning it. That buildPizza() method can be as complicated as needed. Anyone using this class doesn't know or care about any of that. The method could also create different classes for different situations if that's what our brave pizza-parlor-owner-slash-Symfony-dev needs for their app.

Creating the Builder Class

Ok, let's create a builder in our project. Head over to GameApplication and go down to createCharacter(). The problem is that we're building four different Character objects and passing quite a bit of data to configure each one. And, what if we need to create these Character objects in other places in our code? They're not super easy to build right now. We could make some sub-class of Character that can set this data up automatically, like by calling the parent constructor. But, like we talked about with the strategy pattern, that could get really ugly when we start having odd combinations of things like a mage-archer with an IceBlockType shield class.

And what if creating a Character object was even more difficult? Like, if it required making database queries or other operations? Our goal is to make the instantiation of Character objects easier and more clear. And we can accomplish that by creating a builder class.

Add a src/Builder/ directory for organization and, inside of that, a new PHP class called CharacterBuilder. I'm creating this class but I am not creating a corresponding interface. Builder classes often implement an interface like CharacterBuilderInterface, but they don't need to. Later, we'll talk about why you might decide to add an interface in some situations.

8 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 4
class CharacterBuilder
{
}

Methods and Method Chaining

Okay, inside, we get to create whatever methods we want to allow the outside world to craft characters. For example, public function setMaxHealth(), which will accept an int $maxHealth argument. I'm going to leave this method empty for the moment... but it well eventually return itself: it will return CharacterBuilder. This is really common in the builder pattern because it allows method chaining, also known as a "fluent interface". But, it's not a requirement of the pattern.

All right, let's quickly fill in a few more methods, like setBaseDamage()... and the last two are the armor and attack types. So say setAttackType(). Remember, attack types are objects. But instead of allowing an AttackType interface argument, I'm going to accept a string argument called $attackType. Why? I don't have to this, but I'm trying to make it as easy as possible to create characters. So instead of making someone else instantiate the attack types, I'm going to allow them to pass a simple string - like the word bow - and, in a few minutes, we will handle the complexity of instantiating the object for them.

Okay, copy that, and do the same for setArmorType().

34 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 6
class CharacterBuilder
{
public function setMaxHealth(int $maxHealth): self
{
// ... line 11
}
public function setBaseDamage(int $baseDamage): self
{
// ... line 16
}
public function setAttackType(string $attackType): self
{
// ... line 21
}
public function setArmorType(string $armorType): self
{
// ... line 26
}
// ... lines 29 - 32
}

And... that's it! Those are the only four things that you can control in a character.

The Creational Method

The final method that our builder needs is the one that will actually build the Character. You can call this anything you want, how about buildCharacter(). And it is, of course, going to return a Character object.

34 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 4
use App\Character\Character;
// ... line 6
class CharacterBuilder
{
// ... lines 9 - 28
public function buildCharacter(): Character
{
// ... line 31
}
}

To store the character stats, we're going to create four properties, which I'll paste in: private int $maxHealth, private int $baseDamage, and then private string $attackType and private string $armorType. Then, in each method, assign that property and return $this. We'll do that for $baseDamage... $attackType... and $armorType.

80 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 14
class CharacterBuilder
{
private int $maxHealth;
private int $baseDamage;
private string $attackType;
private string $armorType;
public function setMaxHealth(int $maxHealth): self
{
$this->maxHealth = $maxHealth;
return $this;
}
public function setBaseDamage(int $baseDamage): self
{
$this->baseDamage = $baseDamage;
return $this;
}
public function setAttackType(string $attackType): self
{
$this->attackType = $attackType;
return $this;
}
public function setArmorType(string $armorType): self
{
$this->armorType = $armorType;
return $this;
}
// ... lines 49 - 78
}

Beautiful! The buildCharacter() method is fairly straightforward: we do whatever ugly work needed to create the Character. So I'll say return new Character() passing $this->maxHealth and $this->baseDamage. The last two arguments require objects... so they're a bit more complex. But that's ok! I don't mind if my builder gets a little complicated.

Doing some Heavy Lifting

I'll go to the bottom of this class and paste in two new private methods. These handle creating the AttackType and ArmorType objects. Except... I need a bunch of use statements for this, which I forgot. Whoops. So I'm going to re-type the end of these classes and hit "tab" to add those use statements. There we go!

Okay, we can now use the two new private methods to map the strings to objects. This is the heavy lifting - and the real value - of CharacterBuilder. Say $this->createAttackType() and $this->createArmorType().

80 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 5
use App\ArmorType\IceBlockType;
use App\ArmorType\LeatherArmorType;
use App\ArmorType\ShieldType;
// ... line 9
use App\AttackType\BowType;
use App\AttackType\FireBoltType;
use App\AttackType\TwoHandedSwordType;
// ... lines 13 - 14
class CharacterBuilder
{
// ... lines 17 - 49
public function buildCharacter(): Character
{
return new Character(
$this->maxHealth,
$this->baseDamage,
$this->createAttackType(),
$this->createArmorType(),
);
}
private function createAttackType(): AttackType
{
return match ($this->attackType) {
'bow' => new BowType(),
'fire_bolt' => new FireBoltType(),
'sword' => new TwoHandedSwordType(),
default => throw new \RuntimeException('Invalid attack type given')
};
}
private function createArmorType(): ArmorType
{
return match ($this->armorType) {
'ice_block' => new IceBlockType(),
'shield' => new ShieldType(),
'leather_armor' => new LeatherArmorType(),
default => throw new \RuntimeException('Invalid armor type given')
};
}
}

And... our builder is done! Next: let's use this in GameApplication. Then, we'll make our builder even more flexible (but not more difficult to use) by accounting for characters that use multiple attack types.