Strategy Pattern
The first pattern we'll talk about is the "strategy pattern". This is a behavioral pattern that helps organize code into separate classes that can then interact with each other.
Definition
Let's start with the technical definition:
The strategy pattern defines a family of algorithms, encapsulates each one and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
If that made sense to you, congrats! You get to teach the rest of the tutorial!
Let's try that again. Here's my definition:
The strategy pattern is a way to allow part of a class to be rewritten from the outside.
Imaginary Example
Let's talk about an imaginary example before we start coding. Suppose we have a PaymentService
that does a bunch of stuff... including charging people via credit card. But now, we discover that we need to use this exact same class to allow people to pay via PayPal... or via pirate treasure - that sounds more fun.
Anyways, how can we do that? The strategy pattern! We would allow a new PaymentStrategyInterface
object to be passed into PaymentService
and then we would call that.
Next, we would create two classes that implement the new interface: CreditCardPaymentStrategy
and PiratesBootyPaymentStrategy
. That's it! We now have control of which class we pass in. Yep! We just made part of the code inside PaymentService
controllable from the outside.
The Real Example
With that in mind, let's actually code this pattern.
Right now, we have three characters that are created inside of GameApplication
. But the fighter
is dominating. To balance the game, I want to add special attack abilities for each character. For example, the mage
will be able to cast spells.
// ... lines 1 - 6 | |
class GameApplication | |
{ | |
// ... lines 9 - 37 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
'fighter' => new Character(90, 12, 0.25), | |
'archer' => new Character(80, 10, 0.15), | |
'mage' => new Character(70, 8, 0.10), | |
default => throw new \RuntimeException('Undefined Character'), | |
}; | |
} | |
// ... lines 47 - 68 | |
} |
Currently, the attack functionality is pretty boring: we take the character's baseDamage
then use this cool Dice::roll()
function to roll a six-sided die for some randomness.
// ... lines 1 - 6 | |
class Character | |
{ | |
// ... lines 9 - 25 | |
public function attack(): int | |
{ | |
$this->currentStamina -= (25 + Dice::roll(20)); | |
if ($this->currentStamina <= 0) { | |
// can't attack this turn | |
$this->currentStamina = self::MAX_STAMINA; | |
return 0; | |
} | |
return $this->baseDamage + Dice::roll(6); | |
} | |
// ... lines 38 - 70 | |
} |
But when a mage
casts a spell, the damage it causes will be much more variable: sometimes a spell works really well but... other times it makes like tiny fireworks that do almost zero damage.
Basically, for the mage, we need completely different code for calculating damage.
Pass in an Option?
So how can we do this? How can we allow one character - the mage - to have different damage logic? The first idea that comes to my mind is to pass a flag into the character's constructor, like $canCastSpells
. Then in the attack()
method, add an if
statement so that we have both types of attacks.
Cool... but what if an archer
needs yet a different type of attack? We'd then have to pass another flag and we'd end up with three variations inside of attack()
. Yikes.
Sub-Class?
Ok then, another solution might be that we sub-class Character
. We create a MageCharacter
that extends Character
, then override the attack()
method entirely. But, darn it! We don't want to override all of attack()
, we just want to replace part of it. We could get fancy by moving the part we want to reuse into a protected function so that we can call it from our sub-class... but this is getting a little ugly. Ideally we can solve problems without inheritance whenever possible.
Creating the "strategy" Interface
So let's back up. What we really want to do is allow this code to be different on a character-by-character basis. And that is exactly what the strategy pattern allows.
Let's do this! The logic that we need the flexibility to change is this part here, where we determine how much damage an attack did.
Ok, step 1 to the pattern is to create an interface that describes this work. I'm going to add a new AttackType/
directory to organize things. Inside, create a new PHP class, change the template to "Interface", and call it AttackType
.
Cool! Inside, add one public function
called, how about, performAttack()
. This will accept the character's $baseDamage
- because that might be useful - then return the final damage that should be applied.
// ... lines 1 - 4 | |
interface AttackType | |
{ | |
public function performAttack(int $baseDamage): int; | |
} |
Awesome!
Adding Implementation of the Interface
Step 2 is to create at least one implementation of this interface. Let's pretend our mage
has a cool fire attack. Inside the same directory, create a class called FireBoltType
... and make it implement AttackType
. Then, go to "Code -> Generate" - or "command" + "N" on a Mac - and select "Implement Methods" as a shortcut to add the method we need.
// ... lines 1 - 6 | |
class FireBoltType implements AttackType | |
{ | |
public function performAttack(int $baseDamage): int | |
{ | |
// ... line 11 | |
} | |
} |
For the magic attack, return Dice::roll(10)
3 times. So the damage done is the result of rolling 3 10-sided dice.
// ... lines 1 - 8 | |
public function performAttack(int $baseDamage): int | |
{ | |
return Dice::roll(10) + Dice::roll(10) + Dice::roll(10); | |
} | |
// ... lines 13 - 14 |
And... our first attack type is done! While we're here, let's create two others. I'll add a BowType
... and paste in some code. You can copy this from the code block on this page. This attack has a chance of doing some critical damage. Finally, add a TwoHandedSwordType
... and I'll paste in that code as well. This one is pretty straightforward: it's the $baseDamage
plus some random rolls.
// ... lines 1 - 4 | |
use App\Dice; | |
class BowType implements AttackType | |
{ | |
public function performAttack(int $baseDamage): int | |
{ | |
$criticalChance = Dice::roll(100); | |
return $criticalChance > 70 ? $baseDamage * 3 : $baseDamage; | |
} | |
} |
// ... lines 1 - 4 | |
use App\Dice; | |
class TwoHandedSwordType implements AttackType | |
{ | |
public function performAttack(int $baseDamage): int | |
{ | |
$twoHandledSwordDamage = Dice::roll(12) + Dice::roll(12); | |
return $baseDamage + $twoHandledSwordDamage; | |
} | |
} |
Passing in and Using the Strategy
We're ready for the 3rd and final step for this pattern: allow an AttackType
interface to be passed into Character
so that we can use it below. So, quite literally, we're going to add a new argument: private
- so it's also a property - type-hinted with the AttackType
interface (so we can allow any AttackType
to be passed in) and call it $attackType
.
// ... lines 1 - 4 | |
use App\AttackType\AttackType; | |
// ... lines 6 - 7 | |
class Character | |
{ | |
// ... lines 10 - 15 | |
public function __construct( | |
// ... lines 17 - 19 | |
private AttackType $attackType | |
) { | |
// ... line 22 | |
} | |
// ... lines 24 - 72 | |
} |
Below, remove this comment... because now, instead of doing the logic manually, we'll say return
$this->attackType->performAttack($this->baseDamage).
// ... lines 1 - 24 | |
public function attack(): int | |
{ | |
$this->currentStamina -= (25 + Dice::roll(20)); | |
if ($this->currentStamina <= 0) { | |
// can't attack this turn | |
$this->currentStamina = self::MAX_STAMINA; | |
return 0; | |
} | |
return $this->attackType->performAttack($this->baseDamage); | |
} | |
// ... lines 37 - 71 |
And we're done! Our Character
class is now leveraging the strategy pattern. It allows someone outside of this class to pass in an AttackType
object, effectively letting them control just part of its code.
Taking Advantage of our Flexibility
To take advantage of the new flexibility, open up GameApplication
, and inside of createCharacter()
, pass an AttackType
to each of these, like new TwoHandedSwordType()
for the fighter
, new BowType()
for the archer
, and new FireBoltType()
for the mage
.
// ... lines 1 - 4 | |
use App\AttackType\BowType; | |
use App\AttackType\FireBoltType; | |
use App\AttackType\TwoHandedSwordType; | |
// ... lines 8 - 9 | |
class GameApplication | |
{ | |
// ... lines 12 - 40 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
'fighter' => new Character(90, 12, 0.25, new TwoHandedSwordType()), | |
'archer' => new Character(80, 10, 0.15, new BowType()), | |
'mage' => new Character(70, 8, 0.10, new FireBoltType()), | |
// ... line 47 | |
}; | |
} | |
// ... lines 50 - 71 | |
} |
Sweet! To make sure we didn't break anything, head over and try the game.
php bin/console app:game:play
And... woohoo! It's still working!
Adding a Mixed Attack Character
What's great about the "strategy pattern" is that, instead of trying to pass options to Character
like $canCastSpells = true
to configure the attack, we have full control.
To prove it, let's add a new character - a mage archer: a legendary character that has a bow and casts spells. Double threat!
To support this idea of having two attacks, create a new AttackType
called MultiAttackType
. Make it implement the AttackType
interface and go to "Implement Methods" to add the method.
// ... lines 1 - 2 | |
namespace App\AttackType; | |
class MultiAttackType implements AttackType | |
{ | |
// ... lines 7 - 13 | |
public function performAttack(int $baseDamage): int | |
{ | |
// ... lines 16 - 18 | |
} | |
} |
In this case, I'm going to create a constructor where we can pass in an array
of $attackTypes
. To help out my editor, I'll add some PHPDoc above to note that this is an array specifically of AttackType
objects.
// ... lines 1 - 6 | |
/** | |
* @param AttackType[] $attackTypes | |
*/ | |
public function __construct(private array $attackTypes) | |
{ | |
} | |
// ... lines 13 - 21 |
This class will work by randomly choosing between one of its available $attackTypes
. So, down here, I'll say $type = $this->attackTypes[]
- whoops! I meant to call this attackTypes
with a "s" - then array_rand($this->attackTypes)
. Return $type->performAttack($baseDamage)
.
// ... lines 1 - 13 | |
public function performAttack(int $baseDamage): int | |
{ | |
$type = $this->attackTypes[array_rand($this->attackTypes)]; | |
return $type->performAttack($baseDamage); | |
} | |
// ... lines 20 - 21 |
Done! This is a very custom attack, but with the "strategy pattern", it's no problem. Over in GameApplication
, add the new mage_archer
character... and I'll copy the code above. Let's have this be... 75, 9, 0.15
. Then, for the AttackType
, say new MultiAttackType([])
passing new BowType()
and new FireBoltType()
.
// ... lines 1 - 6 | |
use App\AttackType\MultiAttackType; | |
// ... lines 8 - 10 | |
class GameApplication | |
{ | |
// ... lines 13 - 41 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
// ... lines 45 - 47 | |
'mage_archer' => new Character(75, 9, .15, new MultiAttackType([new BowType(), new FireBoltType()])), | |
}; | |
} | |
// ... lines 51 - 73 | |
} |
Sweet! Below, we also need to update getCharacterList()
so that it shows up in our character selection list.
// ... lines 1 - 51 | |
public function getCharactersList(): array | |
{ | |
return [ | |
'fighter', | |
'mage', | |
'archer', | |
'mage_archer' | |
]; | |
} | |
// ... lines 61 - 75 |
Okay, let's check out the legendary new character:
php bin/console app:game:play
Select mage_archer
and... oh! A stunning victory against a normal archer
. How cool is that?
Next, let's use the "strategy pattern" one more time to make our Character
class even more flexible. Then, we'll talk about where you can see the "strategy pattern" in the wild and what specific benefits it gives us.
Hi,
Thank you for this course. I’ve just come from the Solid course, which was another great one. I’m curious why classic or abstract inheritance isn’t being used here? I understand the principle of “Composition over Inheritance,” but in your case, where archer, fighter, and mage are all character subtypes, wouldn’t inheritance be beneficial? Could you explain when I should prefer Inheritance over Composition with real use case ?
Thanks.