Mejoras en el constructor
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login Subscribe¡La primera versión de nuestra clase constructora está terminada! Aunque, en GameApplication
, el mage_archer
tiene dos tipos de ataque diferentes. Nuestro CharacterBuilder
no admite eso ahora mismo... pero lo añadiremos en un momento.
¿Limpiar el estado después de construir?
Ah, ¡una cosa más sobre la clase constructora! En el método "construir", después de crear el objeto, puedes elegir "reiniciar" el objeto constructor. Por ejemplo, podríamos establecer el Character
en una variable, y luego, antes de devolverlo, restablecer el$maxHealth
y todas las demás propiedades a su estado original. ¿Por qué haríamos esto? Porque permitiría utilizar un único constructor una y otra vez para crear muchos objetos, o personajes en este caso.
// ... lines 1 - 14 | |
class CharacterBuilder | |
{ | |
// ... lines 17 - 49 | |
public function buildCharacter(): Character | |
{ | |
return new Character( | |
$this->maxHealth, | |
$this->baseDamage, | |
$this->createAttackType(), | |
$this->createArmorType(), | |
); | |
} | |
// ... lines 59 - 78 | |
} |
Sin embargo, no voy a hacer eso... lo que significa que un solo CharacterBuilder
estará destinado a ser utilizado una sola vez para construir un solo personaje. Puedes elegir cualquiera de las dos opciones en tu aplicación: no hay una forma correcta o incorrecta para el patrón constructor.
Utilizar el Constructor
Muy bien, ¡vamos a utilizarlo! Dentro de GameApplication
, primero, sólo para facilitar la vida, voy a crear un private function
en la parte inferior llamadocreateCharacterBuilder()
que devolverá CharacterBuilder
. Dentro,return new CharacterBuilder()
.
// ... lines 1 - 4 | |
use App\Builder\CharacterBuilder; | |
// ... lines 6 - 14 | |
class GameApplication | |
{ | |
// ... lines 17 - 78 | |
private function createCharacterBuilder(): CharacterBuilder | |
{ | |
return new CharacterBuilder(); | |
} | |
} |
Eso va a estar bien porque... aquí arriba, en createCharacter()
, podemos usar eso. Voy a borrar lo viejo... y ahora, usar la forma fluida de hacer caracteres: $this->createCharacterBuilder()
, ->setMaxHealth(90)
,->setBaseDamage(12)
, ->setAttackType('sword')
y ->setArmorType('shield')
. Ah, y, aunque no lo he hecho, estaría bien añadir constantes en el constructor para estas cadenas, como sword
y shield
.
Por último, llama a ->buildCharacter()
para... ¡construir ese carácter!
// ... lines 1 - 7 | |
class GameApplication | |
{ | |
// ... lines 10 - 38 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
'fighter' => $this->createCharacterBuilder() | |
->setMaxHealth(90) | |
->setBaseDamage(12) | |
->setAttackType('sword') | |
->setArmorType('shield') | |
->buildCharacter(), | |
// ... lines 48 - 70 | |
}; | |
} | |
// ... lines 73 - 100 | |
} |
¡Eso está muy bien! Y sería aún más bonito si la creación de un personaje fuera aún más compleja, como si implicara llamadas a la base de datos.
Para ahorrar algo de tiempo, voy a pegar los otros tres personajes, que tienen un aspecto similar. Aquí abajo, para nuestro mage_archer
, estoy utilizando actualmente el tipo de ataquefire_bolt
. Tenemos que volver a añadir una forma de tener tanto fire_bolt
comobow
, pero esto debería funcionar por ahora.
// ... lines 1 - 38 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
// ... lines 42 - 48 | |
'archer' => $this->createCharacterBuilder() | |
->setMaxHealth(80) | |
->setBaseDamage(10) | |
->setAttackType('bow') | |
->setArmorType('leather_armor') | |
->buildCharacter(), | |
'mage' => $this->createCharacterBuilder() | |
->setMaxHealth(70) | |
->setBaseDamage(8) | |
->setAttackType('fire_bolt') | |
->setArmorType('ice_block') | |
->buildCharacter(), | |
'mage_archer' => $this->createCharacterBuilder() | |
->setMaxHealth(75) | |
->setBaseDamage(9) | |
->setAttackType('fire_bolt') // TODO re-add bow! | |
->setArmorType('shield') | |
->buildCharacter(), | |
default => throw new \RuntimeException('Undefined Character') | |
}; | |
} | |
// ... lines 73 - 102 |
¡Vamos a probarlo! En tu terminal, ejecuta:
php bin/console app:game:play
¡Eh! ¡No ha explotado! Eso siempre es una buena señal. Y si lucho como archer
... ¡gané! ¡Nuestra aplicación sigue funcionando!
Permitir varios tipos de ataque
¿Y qué hay de permitir los dos tipos de ataque de nuestro mago_arquero? Bueno, esa es la belleza del patrón constructor. Parte de nuestro trabajo, cuando creamos la clase constructora, es hacer la vida lo más fácil posible a quien utiliza esta clase. Por eso elegí utilizar string
$armorType
y $attackType
en lugar de objetos.
Podemos resolver el manejo de dos AttackTypes
diferentes como queramos. Personalmente, creo que sería genial poder pasar múltiples argumentos. Así que ¡hagámoslo realidad!
En CharacterBuilder
, cambia el argumento a ...$attackTypes
con una "s", utilizando el elegante ...
para aceptar cualquier número de argumentos. Luego, como esto va a contener una matriz, cambia la propiedad a private array $attackTypes
... y aquí abajo, $this->attackTypes = $attackTypes
.
// ... lines 1 - 15 | |
class CharacterBuilder | |
{ | |
// ... lines 18 - 19 | |
private array $attackTypes; | |
// ... lines 21 - 36 | |
public function setAttackType(string ...$attackTypes): self | |
{ | |
$this->attackTypes = $attackTypes; | |
return $this; | |
} | |
// ... lines 43 - 86 | |
} |
Es fácil. A continuación, tenemos que hacer algunos cambios abajo, en buildCharacter()
, como cambiar las cadenas de $attackTypes
por objetos. Para ello, voy a decir $attackTypes =
y... a ponerme un poco elegante. No hace falta que lo hagas, pero voy a utilizar array_map()
y la nueva sintaxis corta de fn
- (string
$attackType) => $this->createAttackType($attackType). Para el segundo argumento de array_map()
-el array que realmente queremos mapear- utiliza$this->attackTypes
.
// ... lines 1 - 50 | |
public function buildCharacter(): Character | |
{ | |
$attackTypes = array_map(fn(string $attackType) => $this->createAttackType($attackType), $this->attackTypes); | |
// ... lines 54 - 65 | |
} | |
// ... lines 67 - 88 |
Ahora, en el método privado, en lugar de leer la propiedad, lee un argumento de $attackType
.
// ... lines 1 - 67 | |
private function createAttackType(string $attackType): AttackType | |
{ | |
return match ($attackType) { | |
// ... lines 71 - 74 | |
}; | |
} | |
// ... lines 77 - 88 |
Vale, podríamos haber hecho esto con un bucle foreach
... y si te gustan más los bucles foreach
, hazlo. Sinceramente, creo que he estado escribiendo demasiado JavaScript últimamente. De todos modos, esto dice básicamente:
Quiero hacer un bucle sobre todas las cadenas de "tipo de ataque" y, para cada una, llamar a esta función en la que cambiamos esa cadena
$attackType
por un objetoAttackType
. A continuación, pon todos esos objetosAttackType
en una nueva variable$attackTypes
.
En otras palabras, ahora se trata de una matriz de objetos AttackType
.
Para terminar, di if (count($attackTypes) === 1)
, y luego$attackType = $attackTypes[0]
para coger el primer y único tipo de ataque. Si no, di $attackType = new MultiAttackType()
pasando por $attackTypes
. Por último, al final, utiliza la variable $attackType
.
// ... lines 1 - 50 | |
public function buildCharacter(): Character | |
{ | |
// ... line 53 | |
if (count($attackTypes) === 1) { | |
$attackType = $attackTypes[0]; | |
} else { | |
$attackType = new MultiAttackType($attackTypes); | |
} | |
// ... line 59 | |
return new Character( | |
// ... lines 61 - 62 | |
$attackType, | |
// ... line 64 | |
); | |
} | |
// ... lines 67 - 88 |
¡Uf! Puedes ver que es un poco feo, ¡pero no pasa nada! Estamos ocultando la complejidad de la creación dentro de esta clase. Y podemos probarla fácilmente de forma unitaria.
Vamos a probarlo. Ejecuta nuestro comando...
./bin/console app:game:play
... seamos un mage_archer
y... ¡impresionante! ¡No hay error! Así que... voy a suponer que todo funciona.
Vale, en GameApplication
, estamos instanciando el CharacterBuilder
manualmente. Pero, ¿qué pasa si el CharacterBuilder
necesita acceder a algunos servicios para hacer su trabajo, como el EntityManager para poder hacer consultas a la base de datos?
A continuación, vamos a hacer más útil este ejemplo viendo cómo manejamos la creación de este objeto CharacterBuilder
en una aplicación Symfony real aprovechando el contenedor de servicios. También hablaremos de las ventajas del patrón constructor.