El patrón de fábrica abstracta
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 SubscribeVamos 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?
// ... 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!
// ... 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!
// ... 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".
// ... 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
.
// ... 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.
// ... 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!
// ... 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.
// ... 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!
// ... 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!
// ... lines 1 - 8 | |
use Symfony\Component\DependencyInjection\Attribute\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.