Buy Access to Course
03.

Estrategia Parte 2: Beneficios y en la naturaleza

|

Share this awesome video!

|

Acabamos de utilizar el Patrón de Estrategia para permitir que cosas ajenas a la clase Character controlen cómo se producen los ataques, creando un AttackType personalizado... y pasándolo después al crear el Character.

¿Convenciones de nombres?

Si has leído sobre este patrón, puede que te preguntes por qué no hemos llamado a la interfaz AttackStrategy como el patrón. La respuesta es... porque no tenemos que hacerlo. En serio, la claridad y el propósito de esta clase son más valiosos que insinuar el nombre de un patrón. Si lo llamáramos "estrategia de ataque"... podría parecer que se encarga de planificar una estrategia de ataque. Eso no es lo que pretendíamos. De ahí nuestro nombre AttackType

9 lines | src/AttackType/AttackType.php
// ... lines 1 - 4
interface AttackType
{
public function performAttack(int $baseDamage): int;
}

Otro ejemplo de patrón de estrategia

Hagamos otro ejemplo rápido de patrón de estrategia para equilibrar aún más nuestros personajes. Quiero poder controlar la armadura de cada personaje más allá del número que se está pasando ahora. Esto se utiliza en receiveAttack() para calcular en cuánto se puede reducir un ataque. Esto estaba bien antes, pero ahora quiero empezar a crear tipos de armadura muy diferentes que tengan cada uno propiedades distintas más allá de un simple número. Tendremos que actualizar nuestro código para permitir esto.

71 lines | src/Character/Character.php
// ... lines 1 - 7
class Character
{
// ... lines 10 - 37
public function receiveAttack(int $damage): int
{
$armorReduction = (int) ($damage * $this->armor);
$damageTaken = $damage - $armorReduction;
$this->currentHealth -= $damageTaken;
return $damageTaken;
}
// ... lines 46 - 69
}

Una vez más, podríamos resolverlo creando subclases, comoCharacterWithShield. Pero ahora puedes ver por qué no es un buen plan. Si además hubiéramos utilizado la herencia para personalizar cómo se producen los ataques, podríamos acabar con clases como TwoHandedSwordWithShieldCharacter oSpellCastingAndBowUsingWearingLeatherArmorCharacter. ¡Yikes!

Así que, en lugar de navegar por esa pesadilla de subclases interminables, utilizaremos el Patrón de Estrategia. Repasemos los tres pasos anteriores. El primer paso es identificar el código que hay que cambiar y crear una interfaz para él.

En nuestro caso, tenemos que determinar en cuánto debe reducirse un ataque. Genial: crea un nuevo directorio ArmorType/ y dentro de él, una nueva clase PHP... que en realidad será una interfaz... y llámala, qué tal, ArmorType.

Para alojar el código de reducción de la armadura, digamos public function getArmorReduction() donde pasaremos el $damage que vamos a hacer, y nos devolverá cuánta reducción de daño debe aplicar la armadura.

9 lines | src/ArmorType/ArmorType.php
// ... lines 1 - 4
interface ArmorType
{
public function getArmorReduction(int $damage): int;
}

El segundo paso es crear al menos una implementación de esto. Crea una nueva clase PHP llamada ShieldType y haz que implemente ArmorType. A continuación, generaré el métodogetArmorReduction(). El escudo es genial porque va a tener un 20% de posibilidades de bloquear completamente un ataque entrante. Crea una variable $chanceToBlock con el valor Dice::roll(100). Luego, si el $chanceToBlock es > 80, vamos a reducir todo el daño. Así que devuelve $damage. Si no, nuestro escudo no tendrá sentido y reducirá el daño en cero. ¡Ay!

19 lines | src/ArmorType/ShieldType.php
// ... lines 1 - 4
use App\Dice;
class ShieldType implements ArmorType
{
/**
* Has 20% to fully block the attack
*/
public function getArmorReduction(int $damage): int
{
$chanceToBlock = Dice::roll(100);
return $chanceToBlock > 80 ? $damage : 0;
}
}

Ya que estamos aquí, vamos a crear otros dos tipos de armadura. La primera es unaLeatherArmorType. Pondré la lógica: absorbe el 20% del daño.

15 lines | src/ArmorType/LeatherArmorType.php
// ... lines 1 - 4
class LeatherArmorType implements ArmorType
{
/**
* Absorbs 25% of the damage
*/
public function getArmorReduction(int $damage): int
{
return floor($damage * 0.25);
}
}

Y luego crearé la genial IceBlockType: una cosita para nuestra gente mágica. También pegaré esa lógica. Esto absorberá dos tiradas de dados de ocho caras sumadas.

17 lines | src/ArmorType/IceBlockType.php
// ... lines 1 - 6
class IceBlockType implements ArmorType
{
/**
* Absorbs 2d8
*/
public function getArmorReduction(int $damage): int
{
return Dice::roll(8) + Dice::roll(8);
}
}

Vale, tercer paso: permite pasar un objeto de la interfaz ArmorType aCharacter... y luego utiliza su lógica. En este caso, no necesitaremos el número $armoren absoluto. En su lugar, añade un argumento private ArmorType $armorType.

72 lines | src/Character/Character.php
// ... lines 1 - 4
use App\ArmorType\ArmorType;
// ... lines 6 - 8
class Character
{
// ... lines 11 - 16
public function __construct(
// ... lines 18 - 20
private ArmorType $armorType
) {
// ... line 23
}
// ... lines 25 - 70
}

Más abajo, en receiveAttack(), di$armorReduction = $this->armorType->getArmorReduction() y pasa $damage. Y para asegurarte de que las cosas no se desvían en negativo, añade un max() después de $damageTakenpasando $damage - $armorReduction y 0.

73 lines | src/Character/Character.php
// ... lines 1 - 38
public function receiveAttack(int $damage): int
{
$armorReduction = $this->armorType->getArmorReduction($damage);
$damageTaken = max($damage - $armorReduction, 0);
// ... lines 44 - 46
}
// ... lines 48 - 73

Listo! Character ahora aprovecha el Patrón de Estrategia... ¡de nuevo! Vamos a aprovecharlo en GameApplication.

Empieza por eliminar el número de armadura en cada uno de ellos. Luego pasaré rápidamente aArmorType: new ShieldType(), new LeatherArmorType(), y new IceBlockType(). Para nuestro mage-archer, que es nuestro personaje raro, lo mantendremos raro dándole un escudo - new ShieldType(). Y también tengo que asegurarme de quitar la armadura para eso también. ¡Perfecto!

78 lines | src/GameApplication.php
// ... lines 1 - 4
use App\ArmorType\IceBlockType;
use App\ArmorType\LeatherArmorType;
use App\ArmorType\ShieldType;
// ... lines 8 - 13
class GameApplication
{
// ... lines 16 - 44
public function createCharacter(string $character): Character
{
return match (strtolower($character)) {
'fighter' => new Character(90, 12, new TwoHandedSwordType(), new ShieldType()),
'archer' => new Character(80, 10, new BowType(), new LeatherArmorType()),
'mage' => new Character(70, 8, new FireBoltType(), new IceBlockType()),
'mage_archer' => new Character(75, 9, new MultiAttackType([new BowType(), new FireBoltType()]), new ShieldType()),
};
}
// ... lines 54 - 76
}

Vamos a probar este equipo. Dirígete y corre:

./bin/console app:game:play

Y... ¡parece que funciona! Vamos a jugar como mage-archer y... ¡qué bien! Bueno, he perdido. Eso no es dulce, ¡pero me esforcé al máximo! Y puedes ver que el "daño infligido" y el "daño recibido" parecen seguir funcionando. ¡Es increíble!

Beneficios del patrón

¡Así que ése es el Patrón de Estrategia! ¿Cuándo lo necesitas? Cuando te encuentres con que necesitas cambiar sólo una parte del código dentro de una clase. ¿Y cuáles son los beneficios? ¡Un montón! A diferencia de la herencia, ahora podemos crear personajes con infinitas combinaciones de comportamientos de ataque y armadura. También podemos intercambiar un AttackType o un ArmorTypeen tiempo de ejecución. Esto significa que podríamos, por ejemplo, leer alguna configuración o variable de entorno y utilizarla dinámicamente para cambiar uno de los tipos de ataque de nuestros personajes sobre la marcha. Eso no es posible con la herencia.

Patrón y principio SOLID

Si has visto nuestro tutorial sobre SOLID, el Patrón de Estrategia es una clara victoria para SRP -el principio de responsabilidad única- y OCP -el principio de abierto-cerrado-. El Patrón de Estrategia nos permite dividir las clases grandes como Characteren otras más pequeñas y centradas, pero que sigan interactuando entre sí. Eso complace a SRP.

Y OCP está contento porque ahora tenemos una forma de modificar o ampliar el comportamiento de la claseCharacter sin cambiar realmente el código que hay dentro. En su lugar, podemos pasar nuevos tipos de armadura y de ataque.

El patrón de estrategia en el mundo real

Por último, ¿dónde podríamos ver este patrón en el mundo real? Un ejemplo, si pulsas "shift" + "shift" y escribes Session.php, es la clase Session de Symfony. ElSession es un simple almacén de valores clave, pero diferentes aplicaciones necesitarán almacenar esos datos en diferentes lugares, como el sistema de archivos o una base de datos.

En lugar de intentar conseguirlo con un montón de código dentro de la propia clase Session, Session acepta un SessionStorageInterface. Podemos pasar cualquier estrategia de almacenamiento de sesión que queramos. Incluso podríamos utilizar variables de entorno para cambiar a un almacenamiento diferente en tiempo de ejecución

¿Dónde más se utiliza el patrón de estrategia? Bueno, es sutil, pero en realidad se utiliza en muchos sitios. Siempre que tengas una clase que acepte una interfaz como argumento del constructor, especialmente si esa interfaz procede de la misma biblioteca, es muy posible que se trate del Patrón de Estrategia. Significa que el autor de la biblioteca decidió que, en lugar de poner un montón de código en medio de la clase, debería abstraerse en otra clase. Y, al hacer una interfaz de tipo, están permitiendo que otra persona pase la implementación -o estrategia- que quiera.

He aquí otro ejemplo. En GitHub, estoy en el repositorio de Symfony. Pulsa "t" y busca JsonLoginAuthenticator. Este es el código detrás del autentificador de seguridad json_login. Una necesidad común con el JsonLoginAuthenticatores utilizarlo de forma normal... pero luego tomar el control de lo que ocurre en caso de éxito: por ejemplo, controlar el JSON que se devuelve tras la autenticación.

Para permitir eso JsonLoginAuthenticator te permite pasar unAuthenticationSuccessHandlerInterface. Así, en lugar de que esta clase intente averiguar qué hacer en caso de éxito, nos permite pasar una implementación personalizada que nos da el control total.

¿Crees que tienes todo eso? ¡Genial! Hablemos ahora del Patrón Constructor.