Login to bookmark this video
Buy Access to Course
07.

Cadena de responsabilidad

|

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

Es hora del patrón de diseño número dos: el patrón Cadena de Responsabilidad. A veces, no existe una definición oficial para un patrón. Ésta no es una excepción, así que aquí tienes mi definición. En pocas palabras, la Cadena de Responsabilidad es una forma de establecer una secuencia de métodos a ejecutar, donde cada método puede decidir ejecutar el siguiente de la cadena o detener la secuencia por completo.

Cuando tenemos que ejecutar una secuencia de comprobaciones para determinar qué hacer a continuación, este patrón puede ayudarnos a hacerlo. Supongamos que queremos comprobar si un comentario es spam o no, y tenemos cinco algoritmos diferentes para ayudarnos a tomar esa determinación. Si alguno de ellos devuelve true, significa que el comentario es spam y debemos detener el proceso porque ejecutar algoritmos es caro. En una situación como ésta, tenemos que encapsular cada algoritmo en una clase "manejadora", configurar la cadena y ejecutarla.

Ahora bien, si te preguntas qué es una clase "manejadora" o a qué me refiero con "cadena", ¡grandes preguntas! Echemos un vistazo más de cerca a la anatomía del patrón.

Anatomía del patrón

La Cadena de Responsabilidades se compone de tres partes:

En primer lugar, tiene un HandlerInterface, que suele contener dos métodos: setNext() y handle(). El método setNext() recibe un nuevo objeto HandlerInterface. Esto nos permite establecer una secuencia de manejadores, eligiendo qué manejadores queremos en la secuencia y en qué orden aparecen. Esta secuencia se llama cadena. El método handle() es donde ponemos nuestra lógica de negocio.

En segundo lugar están los manejadores concretos, que implementan el método HandlerInterface. Pueden contener el siguiente objeto HandlerInterface (añadido con setNext()) y decidir si debe ser llamado o no. Si no contienen el siguiente manejador, este manejador es el último eslabón de la cadena.

Por último, tenemos un cliente que establece la cadena, asegurándose de que la secuencia está en el orden correcto y activa el primer controlador.

Si ahora tienes más preguntas que cuando empezamos, ¡no te preocupes! Tendrá más sentido cuando lo veamos en acción.

El reto de la vida real

Para nuestro siguiente reto, vamos a aumentar el nivel del jugador. Para ello, recompensaremos a los jugadores con XP extra después de una batalla. Podemos recompensarles de varias formas distintas, pero sólo se debe aplicar una a la vez. Las condiciones para las recompensas de XP son las siguientes:

Uno: Si el jugador es de nivel 1. Dos: Si el jugador ha ganado 3 veces o más seguidas. Y tres: Para añadir algo de aleatoriedad, el jugador lanzará dos dados de seis caras. Ganará si sale un par, pero si el resultado es 7, no.

Cada condición recompensará al jugador con 25 XP.

Bien, ¡hagámoslo! El primer paso que tenemos que dar es crear una interfaz para nuestros manejadores. Dentro del directorio src/, crea una nueva carpeta llamada ChainHandler/. Y dentro de ella, añadiremos una nueva clase PHP llamada... ¿qué tal XpBonusHandlerInterface. Recomiendo incluir el nombre del patrón como parte del nombre de la interfaz para que sea más obvio qué patrón estamos utilizando.

Ahora podemos añadir el primer método - public function handle() - y los argumentos - Character $player y FightResult $fightResult. Estos son los dos objetos necesarios para calcular todas las condiciones anteriores.

Y no olvides añadir el tipo de retorno int. Éste será el XP.

Para el siguiente método escribe public function setNext(XpBonusHandlerInterface $next): void. Y casi se me olvida añadir la sentencia import Character. Pulsa "Opción" + "Intro" y selecciona "Importar clase".

// ... lines 1 - 4
use App\Character\Character;
use App\FightResult;
interface XpBonusHandlerInterface
{
public function handle(Character $player, FightResult $fightResult): int;
public function setNext(XpBonusHandlerInterface $next): void;
}

Bien, ¡la interfaz está lista! Es hora de añadir algunos manejadores. Dentro del directorio ChainHandler/, añade una clase PHP. La primera condición para recompensar a los jugadores es que sean de nivel 1, así que la llamaremos LevelHandler. Ahora tenemos que implementar el XpBonusHandlerInterface. ¡Ya hemos visto esto antes! Mantén pulsadas las teclas "Opción" + "Enter" para añadir ambos métodos.

33 lines | src/ChainHandler/LevelHandler.php
// ... lines 1 - 4
use App\Character\Character;
use App\FightResult;
// ... lines 7 - 8
class LevelHandler implements XpBonusHandlerInterface
{
// ... lines 11 - 12
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 15 - 25
}
public function setNext(XpBonusHandlerInterface $next): void
{
// ... line 30
}
}

Primero trabajaremos en setNext(). Dentro, escribe $this->next = $next;. Luego, para añadir la propiedad, haz clic en $this->next, pulsa "Opción" + "Intro", y selecciona "Añadir propiedad".

33 lines | src/ChainHandler/LevelHandler.php
// ... lines 1 - 8
class LevelHandler implements XpBonusHandlerInterface
{
private XpBonusHandlerInterface $next;
// ... lines 12 - 27
public function setNext(XpBonusHandlerInterface $next): void
{
$this->next = $next;
}
}

¡Perfecto! Ahora vamos a trabajar en el método handle(). Queremos recompensar al jugador si su nivel es 1, así que escribe if ($player->getLevel() === 1) y, dentro, escribiremos return 25;. Si el nivel del jugador no es 1, llamaremos al siguiente manejador, pero sólo si está establecido, así que escribe if (isset($this->next)). Dentro, escribiremos return $this->next->handle($player, $fightResult). Al final, escribiremos return 0;. Eso significa que hemos llegado al final de la cadena y no se ha aplicado ninguno de los manejadores.

31 lines | src/ChainHandler/LevelHandler.php
// ... lines 1 - 12
public function handle(Character $player, FightResult $fightResult): int
{
if ($player->getLevel() === 1) {
return 25;
}
if (isset($this->next)) {
return $this->next->handle($player, $fightResult);
}
return 0;
}
// ... lines 25 - 31

¡Sigamos así! Añade otra clase PHP para nuestra segunda condición ganadora -cuando el jugador haya ganado 3 o más veces seguidas- y la llamaremos OnFireHandler. Implementa la interfaz... y utiliza el mismo truco con "Opción" + "Intro" para añadir los métodos.

33 lines | src/ChainHandler/OnFireHandler.php
// ... lines 1 - 4
use App\Character\Character;
use App\FightResult;
// ... lines 7 - 8
class OnFireHandler implements XpBonusHandlerInterface
{
// ... lines 11 - 12
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 15 - 25
}
public function setNext(XpBonusHandlerInterface $next): void
{
// ... line 30
}
}

Haremos lo mismo con setNext(). Escribe $this->next = $next, y mantén pulsada "Opción" + "Intro" para añadir la propiedad.

33 lines | src/ChainHandler/OnFireHandler.php
// ... lines 1 - 8
class OnFireHandler implements XpBonusHandlerInterface
{
private XpBonusHandlerInterface $next;
// ... lines 12 - 27
public function setNext(XpBonusHandlerInterface $next): void
{
$this->next = $next;
}
}

En el método handle(), tenemos que comprobar la racha de victorias del jugador. Ese valor está dentro de $fightResult, así que escribe if ($fightResult->getWinStreak() >= 3). Dentro, recompensa al jugador devolviendo 25. A continuación, añade la misma comprobación que antes, llamando al siguiente controlador: if (isset($this->next))... y return $this->next->handle($player, $fightResult).

31 lines | src/ChainHandler/OnFireHandler.php
// ... lines 1 - 12
public function handle(Character $player, FightResult $fightResult): int
{
if ($fightResult->getWinStreak() >= 3) {
return 25;
}
if (isset($this->next)) {
return $this->next->handle($player, $fightResult);
}
return 0;
}
// ... lines 25 - 31

Así que... no me gusta esta repetición. Seguro que hay una forma mejor de hacerlo, ¿verdad? La hay, y hablaremos de ella más adelante, pero por ahora, terminemos este método y devolvamos 0 al final.

31 lines | src/ChainHandler/OnFireHandler.php
// ... lines 1 - 12
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 15 - 22
return 0;
}
// ... lines 25 - 31

Pasemos a la última condición del manejador, en la que tiramos dos dados y comprobamos si hemos sacado un par o un 7. Llamémosla CasinoHandler, ya que estamos apostando un poco. Empezaremos de la misma forma, implementando la interfaz y añadiendo los métodos manteniendo pulsadas las teclas "Opción" + "Intro".

41 lines | src/ChainHandler/CasinoHandler.php
// ... lines 1 - 4
use App\Character\Character;
// ... line 6
use App\FightResult;
// ... lines 8 - 9
class CasinoHandler implements XpBonusHandlerInterface
{
// ... lines 12 - 13
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 16 - 33
}
public function setNext(XpBonusHandlerInterface $next): void
{
// ... line 38
}
}

Después, igual que antes, implementa setNext(). Dentro, escribe $this->next = $nexty añade la propiedad encima.

41 lines | src/ChainHandler/CasinoHandler.php
// ... lines 1 - 9
class CasinoHandler implements XpBonusHandlerInterface
{
private XpBonusHandlerInterface $next;
// ... lines 13 - 35
public function setNext(XpBonusHandlerInterface $next): void
{
$this->next = $next;
}
}

Ahora vamos a trabajar en el método handle(). Tira un par de dados de seis caras escribiendo $dice1 = Dice::roll(6) y $dice2 = Dice::roll(6). Lo primero que tenemos que hacer aquí es comprobar si hemos sacado 7, porque si es así, tenemos que salir inmediatamente. Escribe if ($dice1 + $dice2 === 7) y, dentro, devuelve 0. A continuación, comprobaremos si hemos sacado un par para poder recompensar al jugador. Escribe if ($dice1 === $dice2) y, dentro, devuelve 25. Si no hemos sacado bien, llamaremos al siguiente controlador, así que, una vez más, comprueba si está activado escribiendo if (isset($this->next)). Dentro, escribe return $this->next->handle($player, $fightResult)... y devuelve 0 al final.

41 lines | src/ChainHandler/CasinoHandler.php
// ... lines 1 - 13
public function handle(Character $player, FightResult $fightResult): int
{
$dice1 = Dice::roll(6);
$dice2 = Dice::roll(6);
// exit immediately
if ($dice1 + $dice2 === 7) {
return 0;
}
// The player wins if rolled a pair
if ($dice1 === $dice2) {
return 25;
}
if (isset($this->next)) {
return $this->next->handle($player, $fightResult);
}
return 0;
}
// ... lines 35 - 41

¡Uf! ¡Hemos terminado de implementar nuestros manejadores! Pero antes de probarlo, tendremos que inicializar la cadena. Cuando lo hagamos, veremos una desventaja de este patrón. Hagámoslo a continuación.