Builder Pattern
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.
// ... 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()
.
// ... 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.
// ... 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
.
// ... 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()
.
// ... 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.
This is really enjoyable and very interesting course. Many thanks for your work!
I have a one small advice. Not for the author, but maybe who noticed that in the end of the video, author retyped the class names for generating use statements. Exists another solution:
Alt
+Enter
in intellij, and I'm pretty sure, that the author knows about it. The shortcut allows import without retype, need just place cursor on the class name (or at its end withEnd
key) and hit theAlt
+Enter
and selectImport class
(the first option). Oh, maybe this is one of my favorite shortcuts in PHPStorm =)