Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Strategy Pattern

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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.

Leave a comment!

2
Login or Register to join the conversation

Hello and thanks for everything.
I just wanted to ask, is it ok that all the AttackTypes except the MultiAttack don't have the constructor?
They all implement an interface, but only MultiAttack has a constructor which accepts an array of attack types.
Shouldn't we move the constructor to the interface and make the parameter optional?

Interface AttackType
{
    public function construct(private array $attackTypes = []);
    public functoin performAttack(int $baseDamage): int;
}

sth like this?

Reply

Hey Alireza,

That's a good question :) - It's not common to make the constructor part of an interface, although PHP allows it. In this case, only the MultiAttackType class needs a constructor, so we would complicate the design if we move it into the interface. Besides that, our goal is to allow "anyone" to use an AttackType, but we do not want them to actually instantiate those AttackType objects. That's why we introduced a builder and a sort of factory (CharacterBuilderFactory). You'll see those classes in the following chapters

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!