Buy Access to Course
07.

The Observer Pattern

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time for pattern number three - the observer pattern. Here's the technical definition:

The Definition

The observer pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.

Okay, not bad, but let's try my version:

The observer pattern allows a bunch of objects to be notified by a central object when something happens.

This is the classic situation where you write some code that needs to be called whenever something else happens. And there are actually two strategies to solve this: the observer pattern and the pub-sub pattern. We'll talk about both. But first up - the observer pattern.

Anatomy of Observer

There are two different types of classes that go into creating this pattern. The first is called the "subject". That's the central object that will do some work and then notify other objects before or after that work. Those other objects are the second type, and they're called "observers".

This is pretty simple. Each observer tells the subject that it wants to be notified. Later, the subject loops over all of the observers and "notifies" them... which means it calls a method on them.

The Real-Life Challenge

Back in our app, we're going to make our game more interesting by introducing levels to the characters. Each time you win a fight, your character will earn some XP or "experience points". After you've earned enough points, the character will "level up", meaning it's base stats, like $maxhealth and $baseDamage, will increase.

To write this new functionality, we could put the code right here inside of GameApplication after the fight finishes. So... maybe down here in finishFightResult(), we would do the XP calculation and see if the character can level up:

107 lines | src/GameApplication.php
// ... lines 1 - 8
class GameApplication
{
// ... lines 11 - 88
private function finishFightResult(FightResult $fightResult, Character $winner, Character $loser): FightResult
{
$fightResult->setWinner($winner);
$fightResult->setLoser($loser);
return $fightResult;
}
// ... lines 96 - 105
}

But, to better organize our code, I want to put this new logic somewhere else and use the observer pattern to connect things. GameApplication will be the subject, which means it will be responsible for notifying any observers when a fight finishes.

Another reason, beyond code organization, that someone might choose the observer pattern is if GameApplication lived in a third-party vendor library and that vendor library wanted to give us - the user of the library - some way to run code after a battle finishes... since we wouldn't have the luxury to just hack the code in GameApplication.

Creating the Observer Interface

Ok, step one to this pattern is to create an interface that all the observers will implement. For organization's sake, I'll create an Observer/ directory. Inside, add a new PHP class, make sure "Interface" is selected, and call it, how about, GameObserverInterface... since these classes will be "observing" something related to each game. FightObserverInterface would also have been a good name:

11 lines | src/Observer/GameObserverInterface.php
// ... lines 1 - 2
namespace App\Observer;
// ... lines 4 - 6
interface GameObserverInterface
{
// ... line 9
}

Inside we just need one public method. We can call it anything: how about onFightFinished():

11 lines | src/Observer/GameObserverInterface.php
// ... lines 1 - 4
use App\FightResult;
interface GameObserverInterface
{
public function onFightFinished(FightResult $fightResult): void;
}

Why do we need this interface? Because, in a minute, we're going to write code that loops over all of the observers inside of GameApplication and calls a method on them. So... we need a way to guarantee that each observer has a method, like onFightFinished(). And we can actually pass onFightFinished() whatever arguments we want. Let's pass it a FightResult argument because, if I want to run some code after a fight finishes, it'll probably be useful to know the result of that fight. I'll also add a void return type:

11 lines | src/Observer/GameObserverInterface.php
// ... lines 1 - 4
use App\FightResult;
interface GameObserverInterface
{
public function onFightFinished(FightResult $fightResult): void;
}

Adding the Subscribe Code

Okay, step two: We need a way for every observer to subscribe to be notified on GameApplication. To do that, create a public function called, how about, subscribe(). You can name this anything. This is going to accept any GameObserverInterface, I'll call it $observer and it will return void. I'll fill in the logic in a moment:

118 lines | src/GameApplication.php
// ... lines 1 - 7
use App\Observer\GameObserverInterface;
class GameApplication
{
// ... lines 12 - 89
public function subscribe(GameObserverInterface $observer): void
{
// TODO: Implement subscribe() method.
}
// ... lines 94 - 116
}

The second part, which is optional, is to add a way to unsubscribe from the changes. Copy everything we just did... paste... and change this to unsubscribe():

118 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
// ... lines 12 - 94
public function unsubscribe(GameObserverInterface $observer): void
{
// TODO: Implement unsubscribe() method.
}
// ... lines 99 - 116
}

Perfect!

At the top of the class, create a new array property that's going to hold all of the observers. Say private array $observers = [] and then, to help my editor, I'll add some documentation: @var GameObserverInterface[]:

127 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
/** @var GameObserverInterface[] */
private array $observers = [];
// ... lines 14 - 125
}

Back down in subscribe(), populate this. I'll add a check for uniqueness by saying if (!in_array($observer, $this->observers, true)), then $this->observers[] = $observer:

127 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
// ... lines 12 - 92
public function subscribe(GameObserverInterface $observer): void
{
if (!in_array($observer, $this->observers, true)) {
$this->observers[] = $observer;
}
}
// ... lines 99 - 125
}

Do something similar down in unsubscribe(). Say $key = array_search($observer, $this->observers) and then if ($key !== false) - meaning we did find that observer - unset($this->observers[$key]):

127 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
// ... lines 12 - 99
public function unsubscribe(GameObserverInterface $observer): void
{
$key = array_search($observer, $this->observers, true);
if ($key !== false) {
unset($this->observers[$key]);
}
}
// ... lines 108 - 125
}

Notifying the Observers

Finally, we're ready to notify these observers. Right after the fight ends, finishFightResult() is called. So, right here, I'll say $this->notify($fightResult):

136 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
// ... lines 12 - 108
private function finishFightResult(FightResult $fightResult, Character $winner, Character $loser): FightResult
{
// ... lines 111 - 113
$this->notify($fightResult);
return $fightResult;
}
// ... lines 118 - 134
}

We don't need to do this... but I'm going to isolate the logic of notifying the observers to a new private function down here called notify(). It will accept the FightResult $fightResult argument and return void. Then foreach over $this->observers as $observer. And because we know that those are all GameObserverInterface instances, we can call $observer->onFightFinished() and pass $fightResult:

136 lines | src/GameApplication.php
// ... lines 1 - 9
class GameApplication
{
// ... lines 12 - 128
private function notify(FightResult $fightResult): void
{
foreach ($this->observers as $observer) {
$observer->onFightFinished($fightResult);
}
}
}

And... the subject - GameApplication - is done! By the way, sometimes the code that notifies the observers - so notify() in our case - lives in a public method and is meant to be called by someone outside of this class. That's just a variation on the pattern. Like with many of the small details of these patterns, you can do whatever you feel is best. I'm showing you the way I like to do things.

Next: let's implement an observer class, write the level-up logic, then hook it into our system.