Patrón Abstract Factory
El patrón Abstract Factory, se encuentra dentro de los denominados patrones de creación. Como define el ya clásico Gang of Four, nos permite desacoplar y flexibilizar la lógica de creación permitiendo múltiples implementaciones.
Abstract Factory ofrece una interfaz común para crear una familia de objetos relacionados sin exponer directamente sus clases.
Consideremos el siguiente ejemplo. Un servicio que crea o sobreescribe el fichero que el usuario especifica con el ya clásico “Hello World!”
class HelloWorldFileWriter {
public function writeTo(string $filepath): void {
$file = new SplFileObject($filepath, 'w+');
$file->fwrite('Hello World!');
}
}
$writer = new HelloWorldFileWriter();
$writer->writeTo('file.txt');
Tiene un pequeño inconveniente, este código no se puede testear de forma unitaria, esto es, sin tener que crear un fichero realmente. El servicio esta acoplado directamente a la creación de SplFileObject
.
Podemos extraer la lógica de creación haciendo uso del patrón Factory
class FileFactory {
public function createFile(string $filename, string $openMode) {
return new SplFileObject($filename, $openMode);
}
}
class HelloWorldFileWriter {
private $fileFactory;
public function __construct(FileFactory $fileFactory) {
$this->fileFactory = $fileFactory;
}
public function writeTo(string $filepath): void {
$file = $this->fileFactory->createFile($filepath, 'w+');
$file->fwrite('Hello World!');
}
}
$writer = new HelloWorldFileWriter(new FileFactory());
$writer->writeTo('file.txt');
Haciendo uso de Duck Typing y Mockery podemos reemplazar la abstracción SplFileObject
por un Test Double espía en nuestros tests
class HelloWorldFileWriterTest extends TestCase {
/**
* @test
*/
public function itShouldWriteHelloWorld(): void {
$writer = new HelloWorldFileWriter(
$this->createFileFactoryStubWith(
$file = new FileSpy()
)
);
$writer->writeTo('irrelevant-file.txt');
$this->assertEquals('Hello World!', $file->data);
}
private function createFileFactoryStubWith($file): FileFactory {
$stub = Mockery::mock(FileFactory::class);
$stub->shouldReceive('createFile')->andReturn($file);
return $stub;
}
}
class FileSpy {
public $data;
public function fwrite(string $data): void {
$this->data = $data;
}
}
Multiples Implementaciones
Ahora bien, la abstracción SplFileObject
no se introdujo hasta la versión de PHP 5.1.0. Como ejemplo ilustrativo, imaginemos que algunos de nuestros clientes siguen utilizando la versión de PHP 5.0.0. Podemos crear una nueva implementación para la abstracción del sistema de ficheros basada en los clásicos descriptores de fichero para ellos.
El primer paso es definir una interfaz común para la abstracción del sistema de ficheros con la que exponer un contrato claro e independiente al de SplFileObject
sobre el cual tengamos control total
interface File {
public function write(string $data): void;
}
Encapsulamos la actual implementación de SplFileObject
destinada a clientes con una versión de PHP mayor a la 5.1.0
class SplFile implements File {
private $file;
public function __construct($filename, $openMode) {
$this->file = new SplFileObject($filename, $openMode);
}
public function write(string $data): void {
$this->file->fwrite($data);
}
}
Y definimos una nueva implementación basada en descriptores para los clientes con PHP menor a la 5.1.0
class DescriptorFile implements File {
private $fd;
public function __construct($filename, $openMode) {
$this->fd = fopen($filename, $openMode);
}
public function write(string $data): void {
fwrite($this->fd, $data);
}
public function __destruct() {
fclose($this->fd);
}
}
De la misma forma, podemos crear dos provedores de abstracciones de ficheros. Esto es, podemos crear un Abstract Factory con múltiples implementaciones
interface FileFactory {
public function createFile(string $filename, string $openMode): File;
}
class DescriptorFileFactory implements FileFactory {
public function createFile(string $filename, string $openMode): File {
return new DescriptorFile($filename, $openMode);
}
}
class SplFileFactory implements FileFactory {
public function createFile(string $filename, string $openMode): File {
return new SplFile($filename, $openMode);
}
}
Solo es necesario adaptar nuestro código a la nueva interfaz en el servicio HelloWorldFileWriter
$file->write('Hello World!');
Y en los tests, simplemente cumplir con la interfaz
class FileSpy implements File {
public $data;
public function write(string $data): void {
$this->data = $data;
}
}
Ahora es el cliente el que puede elegir que Factory utilizar según su versión de PHP sin que ello afecte en absoluto a la ejecución de nuestro servicio
// Bootstrap
$writer = new HelloWorldFileWriter(
new SplFileFactory() // PHP > 5.1.0
// new DescriptorFileFactory() // PHP < 5.1.0
);
// Application
$writer->writeTo('file.txt');