Login to bookmark this video
Buy Access to Course
15.

El patrón de fábrica abstracta

|

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

Vamos a subir de nivel nuestro AttackTypeFactory y convertirlo en una fábrica abstracta. Como vimos en el capítulo anterior, una fábrica abstracta nos permite manejar familias de objetos. Para ilustrar esto, vamos a introducir códigos de trucos en el juego que, si los activamos, nos darán unas armas superpoderosas. Oh sí, ¡ahora sí que podemos dominar el juego! Para añadir códigos de trucos, necesitaremos crear otra fábrica y una forma de intercambiarla en tiempo de ejecución. Así que ¡manos a la obra!

Añadir más fábricas

Lo primero que tenemos que hacer es crear una interfaz para nuestras fábricas. En lugar de hacerlo manualmente, te mostraré un pequeño truco que hará que PhpStorm lo haga por ti. Abre AttackTypeFactory, haz clic con el botón derecho en el nombre de la clase, selecciona "Refactorizar" y luego "Extraer interfaz". Cambia el nombre aAttackTypeFactoryInterface y vuelve a hacer clic en "Refactorizar". Ahí está nuestra interfaz, y tiene un método create() tal y como queríamos. Y en AttackTypeFactory, ya está implementado. Muy práctico, ¿verdad?

11 lines | src/Factory/AttackTypeFactoryInterface.php
// ... lines 1 - 6
interface AttackTypeFactoryInterface
{
public function create(string $type): AttackType;
}

El siguiente paso es crear una nueva fábrica para el nuevo "conjunto" o "familia" de objetosAttackType. Dentro del directorio Factory/, añade una nueva clase PHP que llamaremos UltimateAttackTypeFactory. Implementa la interfaz... y añade el métodocreate() manteniendo pulsadas las teclas "Opción" + "Intro" y seleccionando "Añadir stubs de métodos". Limpio esto y... ¡perfecto!

14 lines | src/Factory/UltimateAttackTypeFactory.php
// ... lines 1 - 6
class UltimateAttackTypeFactory implements AttackTypeFactoryInterface
{
public function create(string $type): AttackType
{
}
}

Ahora, para el método create(), añadiremos una declaración match muy similar a la de AttackTypeFactory. Escribe return match ($type) y, dentro, utilizaremos los mismos casos pero devolveremos objetos AttackType diferentes. Por ejemplo, en el caso bow, devolveremos un arma más poderosa: un objetoTitaniumBowType. Pero... espera. Esa clase aún no existe, ¿verdad? ¡No! Pero para ahorrarnos algo de tiempo, esas nuevas clases AttackType ya están preparadas en el directorio tutorial/, en la raíz de nuestro proyecto. Ábrelo, copia la carpeta Ultimate/ y pégala dentro de src/AttackType/.

¡Muy bien! ¡Vamos a terminar esto! Añade un caso sword y devuelvenew SkullBreakerSwordType(). Para el caso fire_bolt, devuelvenew MeteorType(). Y por último, para el caso default, lanza un\RuntimeException() con un mensaje - Invalid attack type. ¡Listo!

22 lines | src/Factory/UltimateAttackTypeFactory.php
// ... lines 1 - 11
public function create(string $type): AttackType
{
return match ($type) {
'bow' => new TitaniumBowType(),
'fire_bolt' => new MeteorType(),
'sword' => new SkullBreakerSwordType(),
default => throw new \RuntimeException('Invalid attack type given')
};
}
// ... lines 21 - 22

A continuación, tenemos que añadir una forma de intercambiar nuestras fábricas en tiempo de ejecución. AbreCharacterBuilder y desplázate hasta su constructor. Podemos ver que ya tiene una dependencia con la clase concreta AttackTypeFactory. Necesitamos que funcione con cualquier fábrica, así que cambia su type hint aAttackTypeFactoryInterface. Después, para que sea intercambiable, necesitaremos un definidor para esta propiedad, así que elimina la sentencia readonly y añade el método definidor moviendo el cursor sobre el nombre de la propiedad, manteniendo pulsadas las teclas "Opción" + "Intro" y seleccionando "Añadir definidor".

108 lines | src/Builder/CharacterBuilder.php
// ... lines 1 - 10
use App\Factory\AttackTypeFactoryInterface;
class CharacterBuilder
{
// ... lines 15 - 20
public function __construct(private AttackTypeFactoryInterface $attackTypeFactory)
{
}
public function setAttackTypeFactory(AttackTypeFactoryInterface $attackTypeFactory): void
{
$this->attackTypeFactory = $attackTypeFactory;
}
// ... lines 29 - 106
}

Añadir códigos trampa

¡Muy bien! ¡Nos estamos acercando! Ahora necesitamos una forma de reproducir nuestros códigos de trucos. Los manejaremos como opciones de línea de comandos, así que abre GameCommand y, debajo del constructor, escribe protected function configure(). Dentro, añade una nueva opción llamando a $this->addOption(). El primer argumento es el nombre de la opción. La llamaremos cheatCode. El segundo argumento es el atajo. Usaremos c. El tercer argumento es el modo, necesitamos que tenga un valor así que pongámoslo InputOption::VALUE_REQUIRED.

126 lines | src/Command/GameCommand.php
// ... lines 1 - 11
use Symfony\Component\Console\Input\InputOption;
// ... lines 13 - 16
class GameCommand extends Command
{
// ... lines 19 - 25
protected function configure()
{
$this->addOption('cheatCode', 'cc', InputOption::VALUE_OPTIONAL, 'You should not see this...');
}
// ... lines 30 - 124
}

A continuación, dentro del método execute(), antes de seleccionar el carácter, comprobaremos si se ha pasado la opción cheatCode y, en caso afirmativo, la activaremos.

Para ello, escribe if ($input->getOption('cheatCode')), y dentro de éste,$this->game->activateCheatCode(), enviando la opción cheatCode como argumento.

126 lines | src/Command/GameCommand.php
// ... lines 1 - 30
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... lines 33 - 42
if ($input->getOption('cheatCode')) {
$this->game->activateCheatCode($input->getOption('cheatCode'));
}
// ... lines 46 - 54
}
// ... lines 56 - 126

Este método aún no existe, así que vamos a crearlo. Sitúa el cursor sobre el nombre del método, mantén pulsadas las teclas "Opción" + "Intro" y selecciona "Añadir método". Bien, cambia el argumento a string $cheatCode... y dentro, utilizaremos unswitch-case por si queremos añadir más códigos de trucos en el futuro. Para ello, di switch ($cheatCode) y dentro de él, añadiremos un case con el valor de nuestro último código de trucos. Hm... ¿cuál sería un buen valor para eso? ¡Oh! ¡Ya sé! Voy a pegar esto porque es un poco largo, pero puede que te resulte familiar. ¿Recuerdas el famoso código Konami? ¡Parece que han vuelto los 90!

232 lines | src/GameApplication.php
// ... lines 1 - 16
class GameApplication
{
// ... lines 19 - 33
public function activateCheatCode(string $cheatCode): void
{
switch ($cheatCode) {
// Famous Konami Code
case 'up-up-down-down-left-right-left-right-b-a-start':
// ... lines 39 - 46
}
// ... lines 48 - 230
}

Bien, dentro del case, vamos a imprimir un mensaje para que sepamos que se ha activado el código de trucos. Luego cambiaremos la fábrica en el CharacterBuilder. Para ello, escribe$this->characterBuilder->setAttackTypeFactory(new UltimateAttackTypeFactory())y añade un break al final. Impresionante.

232 lines | src/GameApplication.php
// ... lines 1 - 33
public function activateCheatCode(string $cheatCode): void
{
switch ($cheatCode) {
// Famous Konami Code
case 'up-up-down-down-left-right-left-right-b-a-start':
$this->characterBuilder->setAttackTypeFactory(new UltimateAttackTypeFactory());
GameApplication::$printer->info('Cheat code activated!!');
break;
// ... lines 43 - 45
}
}
// ... lines 48 - 232

Ahora, puede que estés pensando "¿Y si el UltimateAttackTypeFactory tiene dependencias?" o "¿Y si no es tan sencillo de instanciar?", y es una preocupación válida. Una forma de resolverlo es la misma que comentamos con las clases "estado": aprovechando el atributo AutowireLocator . Otra opción sería crear una fábrica para tus fábricas. ¡Ohh fábrica-cepción! Espero que Skynet no esté escuchando... Vale, podemos terminar esto añadiendo un caso default e imprimir un mensaje Invalid Cheat Code. ¡Perfecto!

232 lines | src/GameApplication.php
// ... lines 1 - 33
public function activateCheatCode(string $cheatCode): void
{
switch ($cheatCode) {
// ... lines 37 - 42
default:
GameApplication::$printer->info('Invalid cheat code - better luck next time!');
break;
}
}
// ... lines 48 - 232

Antes de probarlo, hay un pequeño detalle que tenemos que solucionar. Symfony no sabe qué AttackTypeFactory inyectar en CharacterBuilder porque tenemos más de una implementación de AttackTypeFactoryInterface. Tenemos que decirle a Symfony cuál utilizar por defecto. Para ello, podemos aprovechar el atributo AsAlias. Abre AttackTypeFactory y, encima del nombre de la clase, escribe #[AsAlias()] y pasa AttackTypeFactoryInterface::class como ID. ¡Listo!

24 lines | src/Factory/AttackTypeFactory.php
// ... lines 1 - 8
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(AttackTypeFactoryInterface::class)]
class AttackTypeFactory implements AttackTypeFactoryInterface
// ... lines 13 - 24

¡Vamos a probarlo! Ve a tu terminal y ejecuta:

php bin/console app:game:play -c up-up-down-down-left-right-left-right-b-a-start

¡Eh! ¡Mira eso! Ahí está nuestro mensaje Ultimate cheat code activated!. Y si luchamos... ¡ganamos en sólo dos rondas! ¡Códigos trampa para la victoria!

A continuación: Veamos cómo se utiliza el patrón de fábrica en el mundo real.