Buy Access to Course
02.

Patrón de estrategia

|

Share this awesome video!

|

El primer patrón del que hablaremos es el "patrón de estrategia". Se trata de un patrón de comportamiento que ayuda a organizar el código en clases separadas que pueden interactuar entre sí.

Definición

Empecemos con la definición técnica:

El patrón de estrategia define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo utilizan.

Si eso tiene sentido para ti, ¡felicidades! ¡Te toca enseñar el resto del tutorial!

Vamos a intentarlo de nuevo. Ésta es mi definición:

El patrón de estrategia es una forma de permitir que parte de una clase se reescriba desde el exterior.

Ejemplo imaginario

Hablemos de un ejemplo imaginario antes de empezar a codificar. Supongamos que tenemos unPaymentService que hace un montón de cosas... incluyendo el cobro a la gente mediante tarjeta de crédito. Pero ahora, descubrimos que necesitamos utilizar esta misma clase para permitir que la gente pague a través de PayPal... o a través de un tesoro pirata - eso suena más divertido.

En cualquier caso, ¿cómo podemos hacerlo? ¡El patrón de estrategia! Permitiríamos pasar un nuevo objetoPaymentStrategyInterface a PaymentService y luego lo llamaríamos.

A continuación, crearíamos dos clases que implementen la nueva interfaz:CreditCardPaymentStrategy y PiratesBootyPaymentStrategy. ¡Y ya está! Ahora tenemos el control de la clase que pasamos. ¡Sí! Acabamos de hacer que parte del código dentro dePaymentService sea controlable desde fuera.

El ejemplo real

Con esto en mente, vamos a codificar este patrón.

Ahora mismo, tenemos tres personajes que se crean dentro de GameApplication. Pero el fighter es el que domina. Para equilibrar el juego, quiero añadir habilidades de ataque especiales para cada personaje. Por ejemplo, el mage podrá lanzar hechizos.

70 lines | src/GameApplication.php
// ... lines 1 - 6
class GameApplication
{
// ... lines 9 - 37
public function createCharacter(string $character): Character
{
return match (strtolower($character)) {
'fighter' => new Character(90, 12, 0.25),
'archer' => new Character(80, 10, 0.15),
'mage' => new Character(70, 8, 0.10),
default => throw new \RuntimeException('Undefined Character'),
};
}
// ... lines 47 - 68
}

Actualmente, la función de ataque es bastante aburrida: tomamos elbaseDamage del personaje y luego utilizamos esta genial función Dice::roll() para lanzar un dado de seis caras para conseguir algo de aleatoriedad.

72 lines | src/Character/Character.php
// ... lines 1 - 6
class Character
{
// ... lines 9 - 25
public function attack(): int
{
$this->currentStamina -= (25 + Dice::roll(20));
if ($this->currentStamina <= 0) {
// can't attack this turn
$this->currentStamina = self::MAX_STAMINA;
return 0;
}
return $this->baseDamage + Dice::roll(6);
}
// ... lines 38 - 70
}

Pero cuando un mage lanza un hechizo, el daño que causa será mucho más variable: a veces un hechizo funciona muy bien, pero... otras veces hace como pequeños fuegos artificiales que hacen casi cero daño.

Básicamente, para el mago, necesitamos un código completamente diferente para calcular el daño.

¿Pasar en una opción?

Entonces, ¿cómo podemos hacer esto? ¿Cómo podemos permitir que un personaje -el mago- tenga una lógica de daño diferente? La primera idea que se me ocurre es pasar una bandera al constructor del personaje, como $canCastSpells. Luego, en el método attack(), añadir una declaración if para que tengamos ambos tipos de ataques.

Genial... ¿pero qué pasa si un archer necesita otro tipo de ataque? Entonces tendríamos que pasar otra bandera y acabaríamos con tres variaciones dentro deattack(). Vaya.

¿Subclase?

Bien, entonces otra solución podría ser que subclasificáramos Character. Creamos unMageCharacter que extienda a Character, y luego anulamos el método attack() por completo. Pero, ¡maldita sea! No queremos anular todo attack(), sólo queremos sustituir una parte. Podríamos ponernos elegantes trasladando la parte que queremos reutilizar a una función protegida para poder llamarla desde nuestra subclase... pero esto se está poniendo un poco feo. Lo ideal es que resolvamos los problemas sin herencia siempre que sea posible.

Creación de la interfaz "estrategia"

Así que vamos a retroceder. Lo que realmente queremos hacer es permitir que este código sea diferente carácter por carácter. Y eso es exactamente lo que permite el patrón de estrategia.

¡Hagamos esto! La lógica que necesitamos la flexibilidad de cambiar es esta parte de aquí, donde determinamos cuánto daño hizo un ataque.

Bien, el paso 1 del patrón es crear una interfaz que describa este trabajo. Voy a añadir un nuevo directorio AttackType/ para organizar las cosas. Dentro, crea una nueva clase PHP, cambia el patrón a "Interfaz", y llámalaAttackType.

¡Genial! Dentro, añade un public function llamado, qué tal, performAttack(). Esto aceptará el $baseDamage del personaje -porque eso puede ser útil- y luego devolverá el daño final que debe aplicarse.

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

¡Genial!

Añadir la implementación de la interfaz

El paso 2 es crear al menos una implementación de esta interfaz. Imaginemos que nuestro mage tiene un ataque de fuego genial. Dentro del mismo directorio, crea una clase llamada FireBoltType... y haz que implemente AttackType. A continuación, ve a "Código -> Generar" -o "comando" + "N" en un Mac- y selecciona "Implementar métodos" como atajo para añadir el método que necesitamos.

14 lines | src/AttackType/FireBoltType.php
// ... lines 1 - 6
class FireBoltType implements AttackType
{
public function performAttack(int $baseDamage): int
{
// ... line 11
}
}

Para el ataque mágico, devuelve Dice::roll(10) 3 veces. Así, el daño causado es el resultado de lanzar 3 dados de 10 caras.

14 lines | src/AttackType/FireBoltType.php
// ... lines 1 - 8
public function performAttack(int $baseDamage): int
{
return Dice::roll(10) + Dice::roll(10) + Dice::roll(10);
}
// ... lines 13 - 14

Y... ¡nuestro primer tipo de ataque está hecho! Ya que estamos aquí, vamos a crear otros dos. Añadiré un BowType... y pegaré algo de código. Puedes copiarlo del bloque de código de esta página. Este ataque tiene una posibilidad de hacer daño crítico. Por último, añade un TwoHandedSwordType... y pegaré también ese código. Este es bastante sencillo: es el $baseDamage más algunas tiradas al azar.

16 lines | src/AttackType/BowType.php
// ... lines 1 - 4
use App\Dice;
class BowType implements AttackType
{
public function performAttack(int $baseDamage): int
{
$criticalChance = Dice::roll(100);
return $criticalChance > 70 ? $baseDamage * 3 : $baseDamage;
}
}

16 lines | src/AttackType/TwoHandedSwordType.php
// ... lines 1 - 4
use App\Dice;
class TwoHandedSwordType implements AttackType
{
public function performAttack(int $baseDamage): int
{
$twoHandledSwordDamage = Dice::roll(12) + Dice::roll(12);
return $baseDamage + $twoHandledSwordDamage;
}
}

Pasar y utilizar la estrategia

Estamos listos para el tercer y último paso de este patrón: permitir que una interfaz AttackTypese pase a Character para que podamos utilizarla a continuación. Así que, literalmente, vamos a añadir un nuevo argumento: private -por lo que también es una propiedad-, con el tipo de interfaz AttackType (para que podamos permitir que se pase cualquier AttackType) y llamarlo $attackType.

74 lines | src/Character/Character.php
// ... lines 1 - 4
use App\AttackType\AttackType;
// ... lines 6 - 7
class Character
{
// ... lines 10 - 15
public function __construct(
// ... lines 17 - 19
private AttackType $attackType
) {
// ... line 22
}
// ... lines 24 - 72
}

A continuación, elimina este comentario... porque ahora, en lugar de hacer la lógica manualmente, diremos return $this->attackType->performAttack($this->baseDamage).

71 lines | src/Character/Character.php
// ... lines 1 - 24
public function attack(): int
{
$this->currentStamina -= (25 + Dice::roll(20));
if ($this->currentStamina <= 0) {
// can't attack this turn
$this->currentStamina = self::MAX_STAMINA;
return 0;
}
return $this->attackType->performAttack($this->baseDamage);
}
// ... lines 37 - 71

¡Y ya hemos terminado! Nuestra clase Character está ahora aprovechando el patrón de estrategia. Permite que alguien ajeno a esta clase pase un objeto AttackType, permitiéndole efectivamente controlar sólo una parte de su código.

Aprovechar nuestra flexibilidad

Para aprovechar la nueva flexibilidad, abre GameApplication, y dentro decreateCharacter(), pasa un AttackType a cada uno de ellos, comonew TwoHandedSwordType() para el fighter, new BowType() para el archer, ynew FireBoltType() para el mage.

73 lines | src/GameApplication.php
// ... lines 1 - 4
use App\AttackType\BowType;
use App\AttackType\FireBoltType;
use App\AttackType\TwoHandedSwordType;
// ... lines 8 - 9
class GameApplication
{
// ... lines 12 - 40
public function createCharacter(string $character): Character
{
return match (strtolower($character)) {
'fighter' => new Character(90, 12, 0.25, new TwoHandedSwordType()),
'archer' => new Character(80, 10, 0.15, new BowType()),
'mage' => new Character(70, 8, 0.10, new FireBoltType()),
// ... line 47
};
}
// ... lines 50 - 71
}

¡Qué bien! Para asegurarte de que no hemos roto nada, dirígete y prueba el juego.

php bin/console app:game:play

Y... ¡woohoo! ¡Sigue funcionando!

Añadir un personaje de ataque mixto

Lo bueno del "patrón de estrategia" es que, en lugar de intentar pasar opciones a Character como $canCastSpells = true para configurar el ataque, tenemos el control total.

Para demostrarlo, añadamos un nuevo personaje, un mago arquero: un personaje legendario que tiene un arco y lanza hechizos. ¡Doble amenaza!

Para apoyar esta idea de tener dos ataques, crea un nuevo AttackType llamadoMultiAttackType. Haz que implemente la interfaz AttackType y ve a "Implementar métodos" para añadir el método.

21 lines | src/AttackType/MultiAttackType.php
// ... lines 1 - 2
namespace App\AttackType;
class MultiAttackType implements AttackType
{
// ... lines 7 - 13
public function performAttack(int $baseDamage): int
{
// ... lines 16 - 18
}
}

En este caso, voy a crear un constructor en el que podemos pasar un arrayde $attackTypes. Para ayudar a mi editor, añadiré algo de PHPDoc por encima para señalar que se trata de un array específicamente de objetos AttackType.

21 lines | src/AttackType/MultiAttackType.php
// ... lines 1 - 6
/**
* @param AttackType[] $attackTypes
*/
public function __construct(private array $attackTypes)
{
}
// ... lines 13 - 21

Esta clase funcionará eligiendo aleatoriamente entre uno de sus $attackTypes disponibles. Así que, aquí abajo, diré $type = $this->attackTypes[] - ¡Ups! Quería llamar a estoattackTypes con una "s" - entonces array_rand($this->attackTypes). Devuelve$type->performAttack($baseDamage).

21 lines | src/AttackType/MultiAttackType.php
// ... lines 1 - 13
public function performAttack(int $baseDamage): int
{
$type = $this->attackTypes[array_rand($this->attackTypes)];
return $type->performAttack($baseDamage);
}
// ... lines 20 - 21

Ya está Este es un ataque muy personalizado, pero con el "patrón de estrategia", no hay problema. En GameApplication, añade el nuevo carácter mage_archer... y copiaré el código anterior. Que sea... 75, 9, 0.15. Entonces, para el AttackType, digamos new MultiAttackType([]) pasando por new BowType() y new FireBoltType().

75 lines | src/GameApplication.php
// ... lines 1 - 6
use App\AttackType\MultiAttackType;
// ... lines 8 - 10
class GameApplication
{
// ... lines 13 - 41
public function createCharacter(string $character): Character
{
return match (strtolower($character)) {
// ... lines 45 - 47
'mage_archer' => new Character(75, 9, .15, new MultiAttackType([new BowType(), new FireBoltType()])),
};
}
// ... lines 51 - 73
}

¡Qué bien! A continuación, también tenemos que actualizar getCharacterList() para que aparezca en nuestra lista de selección de personajes.

75 lines | src/GameApplication.php
// ... lines 1 - 51
public function getCharactersList(): array
{
return [
'fighter',
'mage',
'archer',
'mage_archer'
];
}
// ... lines 61 - 75

Bien, vamos a comprobar el nuevo personaje legendario:

php bin/console app:game:play

Selecciona mage_archer y... ¡oh! una impresionante victoria contra un archer normal. ¿Cómo de genial es eso?

A continuación, vamos a utilizar el "patrón de estrategia" una vez más para que nuestra clase Charactersea aún más flexible. A continuación, hablaremos de dónde puedes ver el "patrón de estrategia" en la naturaleza y qué ventajas concretas nos aporta.