Keyvan Akbary Seguir

Keyvan Akbary

Tech Lead en TransferWise

Autor del libro

Saber más

Patrón Factory

Probablemente uno de los patrones más utilizados en lenguajes de programación modernos. El patrón Factory, una variante actual de los patrones de creación definidos en Gang of Four como Factory Method y Abstract Factory, permite desacoplar la lógica de creación de forma centralizada.

Responsable de crear objetos evitando exponer la lógica de instanciación al cliente.

Como ejemplo, si analizamos el siguiente código

class VendingMachine {
    public function infoFor(int $code): string {
        $description = '';
        $price = 0;
        if ($code === 0) {
            $description = 'delicious chocolate';
            $price = 1;
        } elseif ($code === 1) {
            $description = 'crunchy chips';
            $price = 1.2;
        } elseif ($code === 3) {
            $description = 'tasty sandwich';
            $price = 2.5;
        }

        return $this->format($description, $price);
    }

    private function format(string $description, float $price): string {
        return
            'description: ' . $description . "\n" .
            'price: ' . $price . ' euros';
    }
}
$m = new VendingMachine();
assert($m->infoFor(1) == <<<INFO
description: crunchy chips
price: 1.2 euros
INFO
);

Podemos llegar a la conclusión de que esta implementación tiene algunos problemas. Una máquina expendedora ofrece productos concretos, sin embargo no hay una unidad que represente un producto en el sistema.

Un producto esta representado por una descripción y un precio que estan esparcidos por el método que muestra la información de producto. Además, resulta que toda esta información referente a los productos se encuentra en el propio método. El método encargado de mostrar la información es el mismo que la crea. Añadir una propiedad más al producto, extender el comportamiento o reutilizar la información en otros lugares complicará más el código.

Podemos definir una interfaz común a todos los productos, y definirlos de la siguiente manera

interface Snack {
    public function description(): string;
    public function price(): float;
}

class Chocolate implements Snack {
    public function description(): string {
        return 'delicious chocolate';
    }

    public function price(): float {
        return 1;
    }
}

class Chips implements Snack {
    public function description(): string {
        return 'crunchy chips';
    }

    public function price(): float {
        return 1.2;
    }
}

class Sandwich implements Snack {
    public function description(): string {
        return 'tasty sandwich';
    }

    public function price(): float {
        return 2.5;
    }
}

Parece que ahora nuestro código parece más claro y concisco.

class VendingMachine {
    public function infoFor(int $code): string {
        $snack = null;
        if ($code === 0) {
            $snack = new Chocolate;
        } elseif ($code === 1) {
            $snack = new Chips;
        } elseif ($code === 3) {
            $snack = new Sandwich;
        }

        return $this->format($snack);
    }

    private function format(Snack $snack): string {
        return
            'description: ' . $snack->description() . "\n" .
            'price: ' . $snack->price() . ' euros';
    }
}

La lógica de creación de productos esta fuertemente acoplada con el método responsable de mostrar la propia información. La única forma de incorporar un nuevo producto es la de incorporar un nuevo bloque elseif a este método.

No es responsabilidad del método que muestra la información de producto la de crear los propios productos.

Desacoplando la lógica de creación

Haciendo uso del patrón Factory, podemos extraer la lógica de creación a una clase dedicada exclusivamente a ello.

class SnackFactory {
    public function create(int $code): Snack {
        switch($code) {
            case 0:
                return new Chocolate;
            case 1:
                return new Chips;
            case 2:
                return new Sandwich;
        }

        throw new Exception('No snack for code ' . $code);
    }
}

El código cliente queda entonces liberado de la lógica de creación.

class VendingMachine {
    private $snackFactory;

    public function __construct(SnackFactory $snackFactory) {
        $this->snackFactory = $snackFactory;
    }

    public function infoFor(string $code): string {
        return $this->format($this->snackFactory->create($code));
    }

    private function format(Snack $snack): string {
        return
            'description: ' . $snack->description() . "\n" .
            'price: ' . $snack->price() . ' euros';
    }
}

Añadir o eliminar un producto del catálogo es tan sencillo como modificar la Factory. Cambios en el catálogo ya no afectarán a la máquina expendedora. Ahora la lógica de creación esta desacoplada de la lógica de negocio y puede evolucionar de forma independiente.

Testing

Como beneficio añadido, el hecho de desacoplar la lógica de creación nos permite reemplazar la Factory por un test double en nuestros tests. Ahora podemos forzar un determinado flujo en el System Under Test para probar todos los casos.

class VendingMachineTest extends TestCase {
    /**
     * @test
     */
    public function itShouldComposeSnackInfo(): void {
        $vendingMachine = new VendingMachine($this->createSnackFactoryStubWith(new SnackStub));

        $expected = <<<INFO
description: irrelevant
price: 0 euros
INFO;

        $this->assertEquals($expected, $vendingMachine->infoFor(0));
    }

    private function createSnackFactoryStubWith(Snack $snack): SnackFactory {
        $stub = Mockery::mock(new SnackFactory());
        $stub->shouldReceive('create')->andReturn($snack);

        return $stub;
    }
}

class SnackStub implements Snack {
    public function description(): string {
        return 'irrelevant';
    }

    public function price(): float {
        return 0;
    }
}

No tengo por qué acoplar mi test a un producto real. Sabiendo que mi test va a probar la información que ofrece la máquina expendedora sobre un determinado producto, me basta con un generar un Stub para este caso concreto.

Referencias

¿Ves algo raro? ¡Editame!

Copyright © 2022