Tell Don't Ask
Published on

Adapter pattern - Patterns in real world #1

Authors

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.