- Published on
Adapter pattern - Patterns in real world #1
- Authors
-
-
- Name
- TellDontAskCom
- @telldontaskcom
-
Arrange
Let's have a following setup, an event named GoodPlayersAreStolenUnderOurNose
hooked up to listener
named SendBadNewsToTheOwner
. The event would look something like following:
namespace App\Events;
// imports
class GoodPlayersAreStolenUnderOurNose
{
// traits
public function __construct(
public Collection $players
) {}
// broadcastOn method
}
In real world listener would be far more complicated, to the point where you would not believe how complex sending a notification can be, but let's pretend that it would look something like following:
namespace App\Listeners;
// imports
class SendBadNewsToTheOwner implements ShouldQueue
{
public function handle(GoodPlayersAreStolenUnderOurNose $event): void
{
User::owner()
->first()
->notify(new BadNewsNotification(
$event->players,
GoodPlayersAreStolenUnderOurNose::class
));
}
}
All looks fine and dandy, but now they want to send the same notification via the same listener when PlayerGotInjured
event is fired. That event would look something like following:
namespace App\Events;
// imports
class PlayerGotInjured
{
// traits
public function __construct(
public Player $player
) {}
// broadcastOn method
}
So some inexperienced developers would do following:
namespace App\Listeners;
// imports
class SendBadNewsToTheOwner implements ShouldQueue
{
public function handle($event): void
{
$players = $event instanceof PlayerGotInjured
? collect($event->player)
: $event->players;
$notificationType = $event instanceof PlayerGotInjured
? PlayerGotInjured::class
: GoodPlayersAreStolenUnderOurNose::class;
User::owner()
->first()
->notify(new BadNewsNotification(
$players,
$notificationType
));
}
}
Damn that looks ugly. What would it look like if we would have to add more logic to it. The biggest issue here is that
we are making decisions inside of SendBadNewsToTheOwner
class for our event classes. By doing so, we are
violating Tell Don't Ask
principle and breaking encapsulation.
Act
First let's see how we can easily fix this. Yes, you've guessed it, we need to add an interface.
namespace App\Contracts;
// imports
interface BadNewsEvent
{
public function getFQN(): string;
public function getPlayers(): Collection;
}
Now we add the BadNewsEvent
interface to the PlayerGotInjured
event. The best thing about it is that we are not
converting one player to collection outside the PlayerGotInjured
class. We are not transforming internal details
outside the class which is holding it.
namespace App\Events;
// imports
class PlayerGotInjured implements BadNewsEvent
{
// traits
public function __construct(
protected Player $player
) {}
public function getFQN(): string
{
return self::class;
}
public function getPlayers(): Collection
{
return collect($this->player);
}
// broadcastOn method
}
Now let's do the same for the GoodPlayersAreStolenUnderOurNose
class.
namespace App\Events;
// imports
class GoodPlayersAreStolenUnderOurNose implements BadNewsEvent
{
// traits
public function __construct(
protected Collection $players
) {}
public function getFQN(): string
{
return self::class;
}
public function getPlayers(): Collection
{
return $this->players;
}
// broadcastOn method
}
Finally, we can simplify the SendBadNewsToTheOwner
listener.
namespace App\Listeners;
// imports
class SendBadNewsToTheOwner implements ShouldQueue
{
public function handle(BadNewsEvent $event): void
{
User::owner()
->first()
->notify(new BadNewsNotification(
$event->getPlayers(),
$event->getFQN()
));
}
}
This looks great, and we are more-less back to our original implementation for the SendBadNewsToTheOwner
listener.
We've seen how we can easily fix the issue if we own both events. Also, this approach does not require any changes to
the places where those events are being dispatched.
Now what if PlayerGotInjured
event was coming from a framework or package we are using? We can not go into vendor
folder and change that class. Also, it would be stupid to fork the package just to get this the way we need it. Now
comes into play the real point of this post, the Adapter pattern.
So PlayerGotInjured
would be under different namespace.
namespace Package\Events;
// imports
class PlayerGotInjured
{
// traits
public function __construct(
public Player $player
) {}
// broadcastOn method
}
Let's create PlayerGotInjuredAdapter
class. Not that it has to implement the BadNewsEvent
interface and return fully
qualified name for the PlayerGotInjured
class.
namespace App\Adapters;
// imports
class PlayerGotInjuredAdapter implements BadNewsEvent
{
public function __construct(
protected PlayerGotInjured $playerGotInjured
) {}
public function getFQN(): string
{
return PlayerGotInjured::class;
}
public function getPlayers(): Collection
{
return collect($this->playerGotInjured->player);
}
}
The only missing piece is that in all places where we've dispatched PlayerGotInjured
event we need to wrap it with our
newly created adapter. So this:
event(new PlayerGotInjured($player));
becomes following:
event(new PlayerGotInjuredAdapter(new PlayerGotInjured($player)));
Assert
So by use of simple interface, and later on an adapter pattern, we've managed to:
- avoid any hacking of
vendor
files - preserve encapsulation
- reduce the complexity
- respect
Tell Don't Ask
principle
Remember that adapter pattern just needs to implement the same interface that your code expects and pass in the object that you want to adapt through the constructor.