Factory Pattern
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIt is time for our last design pattern - the Factory pattern. Factory is a creational design pattern that provides a way to hide the details of how your objects are created from the code that uses them. Instead of directly instantiating objects with new
, we use a factory that decides which object to create based on the input.
Pattern Anatomy
The Factory pattern is composed of five parts:
The first part is an interface of the products we want to create. If we wanted to create weapons for our characters, for example, we'd have a WeaponInterface
, and the products would be weapons.
Second are the concrete products that implement the interface. In this example, we would have classes like Sword
, Axe
, Bow
, and so on.
Third is the factory interface. This is optional, but it's super useful when you need to create families of products.
Fourth is the concrete factory that implements the factory interface if we have one. This class knows everything about creating products.
And lastly, we have the client, which uses a factory to create product objects. This class only knows how to use products, but not how they're created, or what specific product it happens to be using.
Factory with Multiple Make Methods
You may be surprised to learn that there's more than one variant of the Factory pattern. The simplest variant is a factory with multiple make
methods - one for each possible product. That factory would look like this:
class WeaponFactory
{
public function makeSword(): WeaponInterface
{
return new Sword(Dice::rollRange(4, 8), 12);
}
public function makeAxe(int $bonusDamage = 0): WeaponInterface
{
return new Axe($bonusDamage + Dice::rollRange(6, 12), 8);
}
}
This variant is useful when the caller already knows which object it needs. It's also easy to have different constructor arguments for each type.
Factory with a Single Make Method
Another approach is to use a single make
method. That will receive an argument and determine which object it needs to create. This is useful when the application is more dynamic. The $type
value may come from the user's input, a request, or something else.
However, there are a couple of downsides to this approach, like losing type safety, since any string can be sent as the type. Luckily, that can be solved with a good test suite, or by transforming the string into an enum
. It's also hard to have different constructor arguments on each type.
Abstract Factory
The last variant we'll talk about is the "Abstract Factory". In this approach, we have multiple factories implementing the same interface, and each concrete factory creates a family of objects. In our character weapons example, we could group weapons based on the material they are made of, like iron or steel, and each factory would only create weapons with that material.
Depending on the application, we can choose which factory will be used based on some config, or swap the factory at runtime based on some event. In our game, we could change the weapons factory whenever the game level changes, which would definitely make things more exciting.
Creating an AttackType Factory
All right! It's time to see the Factory pattern in action! We'll start by creating the simplest factory possible, and then we'll promote it to an abstract factory. We've already created AttackType
objects in a few places in our application. One of them is in the CharacterBuilder
. Open that up and find the createAttackType()
method. If we look at the match
statement, we see that we're creating AttackType
objects based on a string input. So, we're using the single method variant.
// ... lines 1 - 15 | |
class CharacterBuilder | |
{ | |
// ... lines 18 - 92 | |
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') | |
}; | |
} | |
// ... lines 102 - 110 | |
} |
If we open GameInfoCommand
, at the bottom... we have the same match
statement. This duplicate code isn't super ideal because if we ever want to add a new AttackType
or change the constructor arguments, we would have to find and update all of the places we instantiate them. In larger applications, this process would be error-prone and take a ton of time.
Surely there's a better way, right? There is! We're going to refactor this code with a factory. Copy this match
statement code, and inside the src/
directory, create a folder called Factory/
. Inside that, add a new PHP class, and we'll call it AttackTypeFactory
. Awesome! Now we need a method that will create AttackType
objects. Write public function create()
, give it a string $type
argument, and make it return AttackType
objects. In create()
, paste the code and rename the variable to $type
.
// ... lines 1 - 9 | |
class AttackTypeFactory | |
{ | |
public function create(string $type): AttackType | |
{ | |
return match ($type) { | |
'bow' => new BowType(), | |
'fire_bolt' => new FireBoltType(), | |
'sword' => new TwoHandedSwordType(), | |
default => throw new \RuntimeException('Invalid attack type given') | |
}; | |
} | |
} |
Okay, what we've done so far may seem insignificant, but we've accomplished a lot. We've encapsulated how AttackType
objects are created throughout our application and, as a bonus, we also set the foundation for handling families of AttackType
's. We'll talk about that more later on.
The next step is to inject the AttackTypeFactory
into the CharacterBuilder
. Open that up and, at the top, add a constructor with an argument - private readonly AttackTypeFactory $attackTypeFactory
.
// ... lines 1 - 14 | |
use App\Factory\AttackTypeFactory; | |
class CharacterBuilder | |
{ | |
// ... lines 19 - 24 | |
public function __construct(private readonly AttackTypeFactory $attackTypeFactory) | |
{ | |
} | |
// ... lines 28 - 115 | |
} |
Then, find the buildCharacter()
method. That's where we call createAttackType()
. I'll split this onto multiple lines so it's easier to read. And now, replace createAttackType()
with $this->attackTypeFactory->create()
.
// ... lines 1 - 61 | |
$attackTypes = array_map(fn(string $attackType) => $this->attackTypeFactory->create($attackType), $this->attackTypes); | |
// ... lines 63 - 103 |
Perfect! Let's do the same thing in GameInfoCommand
. Open that... and add a constructor. We can let PhpStorm auto-generate that for us so it automatically adds the parent
call. Then we'll inject the factory - private readonly AttackTypeFactory $attackTypeFactory
.
// ... lines 1 - 4 | |
use App\Factory\AttackTypeFactory; | |
// ... lines 6 - 12 | |
class GameInfoCommand extends Command | |
{ | |
public function __construct(private readonly AttackTypeFactory $attackTypeFactory) | |
{ | |
parent::__construct(); | |
} | |
// ... lines 19 - 55 | |
} |
Finally, scroll down, find the computeAverageDamage()
method... and once again, replace createAttackType()
with $this->attackTypeFactory->create()
. Awesome!
// ... lines 1 - 44 | |
private function computeAverageDamage(string $attackTypeString, int $baseDamage = 0): float | |
{ | |
$attackType = $this->attackTypeFactory->create($attackTypeString); | |
// ... lines 48 - 54 | |
} | |
// ... lines 56 - 57 |
I think we're ready to give this a try! Spin over to your terminal and, this time, run the GameInfoCommand
:
php bin/console app:game:info
And... yes! This is great! We can see information about our character classes and their weapons. Let's celebrate by removing all of the duplicated code from CharacterBuilder
and GameInfoCommand
.
All right! We've successfully implemented the Factory pattern! This was the simplest variation of the pattern, so next, let's kick things up a notch and handle families of AttackType
's with an abstract factory.