Patrón template method en PHP
El patrón template method nos permite implementar un proceso en el que hay partes que pueden cambiar, implementando en cada caso únicamente las partes que cambian, pero como siempre, veamos un ejemplo.
En el departamento de administración necesitan un listado de los clientes, así que nos piden si podemos implementarlo en nuestra aplicación. El listado debe tener una cabecera, un cuerpo que incluya un cliente por línea y un pie de página.
Nosotros, desde nuestro sótano, nos ponemos manos a la obra, esto parece sencillo, implementamos un servicio que reciba la lista de clientes, ponga la cabecera, formatee la lista de clientes y finalmente ponga el pie de página, sencillo y les solucionamos una necesidad a nuestros compis.
class GetCustomersReport
{
public function generate(array $customers): string
{
return $this->header() . $this->body($customers) . $this->footer();
}
private function header(): string
{
return "------------------------------------------------------\n"
. "Informe de clientes"
. "\n------------------------------------------------------\n";
}
private function body(array $customers): string
{
$lines = array_map(
fn(Customer $customer) => $customer->getName(),
$customers
);
return implode("\n", $lines);
}
private function footer(): string
{
return "\n------------------------------------------------------\n"
. "Este informe es confidencial y no debe distribuirse"
. "\n------------------------------------------------------\n";
}
}
Ya lo tenemos, subimos a producción e informamos al departamento de administración, que se ponen muy contentos y rápidamente se ponen a probarlo, pero unos días después nos vuelven a contactar y nos explican que hay casos en los que necesitan poder incluir en el informe el número de ventas que se han realizado a cada cliente. Además, necesitan que se pueda elegir si se imprime ese dato o no se imprime cada vez que utilicen ese listado.
Así que no nos queda más remedio que modificar el servicio que saca el listado, y como parece una petición simple, decidimos agregar un swtich que indique si debe incluirse ese campo o no en el listado.
class GetCustomersReport
{
public function generate(array $customers, ?bool $includeCounter = false): string
{
return $this->header() . $this->body($customers, $includeCounter) . $this->footer();
}
private function header(): string
{
return "------------------------------------------------------\n"
. "Informe de clientes"
. "\n------------------------------------------------------\n";
}
private function body(array $customers, bool $includeCounter): string
{
$lines = array_map(
fn(Customer $customer) => $includeCounter
? $customer->getName() . ' - ' . $customer->getSalesCounter
: $customer->getName(),
$customers
);
return implode("\n", $lines);
}
private function footer(): string
{
return "\n------------------------------------------------------\n"
. "Este informe es confidencial y no debe distribuirse"
. "\n------------------------------------------------------\n";
}
}
El problema con añadir este argumento es que no nos va a servir si empiezan a pedir nuevos casos, o se convertirá en un método con un montón de argumentos booleanos, que además son más difíciles de seguir. Imaginad un escenario en el que quieren que se muestren distintos datos, que se formateen de varias maneras, etc. Esto pinta a código inmantenible en unos meses.
Otra opción sería crear dos clases distintas, pero tendríamos que repetir el código de las partes comunes, lo que abre nuevos problemas para el funturo si tenemos que modificar alguna de las partes comunes, así que la descartamos.
De modo que decidimos que podemos refactorizar esto usando el template method ese del que hablaba un compañero hace unos días. Con este patrón, tendríamos una clase abstracta que implementaría la funcionalidad común, delegando en métodos protegidos de las clases hijas las partes que difieren.
En otras palabras, como el propio nombre del patrón indica, crearemos una plantilla del algoritmo, que contendrá partes implementadas por las clases que extiendan a esta, pero veámoslo en código que se explica mejor que yo.
Tendremos una clase abstracta implementando la funcionalidad común y definiendo los métodos que deben implementar las clases hijas. En nuestro caso, la cabecera y el pie serán comunes, y cambiará el cuerpo del informe, así que vamos allá.
<?php
abstract class GetCustomersReport
{
public function generate(array $customers): string
{
return $this->header() . $this->body($customers) . $this->footer();
}
abstract protected function body(array $customers): string;
protected function header(): string
{
return "------------------------------------------------------\n"
. "Informe de clientes"
. "\n------------------------------------------------------\n";
}
protected function footer(): string
{
return "\n------------------------------------------------------\n"
. "Este informe es confidencial y no debe distribuirse"
. "\n------------------------------------------------------\n";
}
}
El método generate implementará la plantilla que llamará a los métodos protegidos que implementa la propia clase y las hijas. Implementaremos los métodos header y footer que son comunes a todos los informes, y definiremos un método abstracto body que deberá ser implementado en las clases que hereden de esta.
Ahora, debemos definir las clases para cada uno de los tipos de listados que tenemos. En nuestro caso, tendremos una clase que implemente el listado de clientes donde únicamente mostraremos el nombre, y otra que incluirá el número de ventas realizadas a cada cliente.
Cada una de estas cláses heredará de la clase abstracta GetCustomersReport e implementará únicamente el comportamiento del body, que es la parte que difiere.
Aquí la implementación del listado que incluye sólo el nombre:
class GetOnlyNameCustomersReport extends GetCustomersReport
{
protected function body(array $customers): string
{
$lines = array_map(
static fn(Customer $customer) => $customer->getName(),
$customers
);
return implode("\n", $lines);
}
}
Aquí la implementación del listado que incluye también el número de ventas:
class GetWithCounterCustomersReport extends GetCustomersReport
{
protected function body(array $customers): string
{
$lines = array_map(
static fn(Customer $customer) => $customer->getName() . ' - ' . $customer->getSalesCounter(),
$customers
);
return implode("\n", $lines);
}
}
Ahora, simplemente deberemos instanciar la clase adecuada para cada listado, bien en dos casos de uso diferentes, o bien creando una factoría que instancie el tipo concreto en cada caso.
$customers = [
new Customer('Pedro Pérez', 16),
new Customer('Antonio López', 1),
];
$generator = GetCustomerReportFactory::getGenerator($type);
$report = $generator->generate($customers);
echo $report;
Con este patrón de diseño, podemos generar listados con todas las diferencias que deseemos, sin reimplementar las partes comunes. Es cierto que en este ejemplo tan sencillo el valor puede ser pequeño, pero el patrón gana importancia cuando las partes comunes son complejas, por ejemplo creando pdfs con FPDF, donde debes crar una cabecera compleja con cuadros, imágenes, etc.