Principio No Te Repitas es uno de los principios en el desarrollo de software cuyo principal objetivo es evitar la duplicación de código.

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system”
Andrew Hunt y David Thomas: The Pragmatic Programmer: From Journeyman to Master

Básicamente, cuando descubres que estás escribiendo el mismo código una y otra vez, todo indica que seguramente habrá una mejor manera de hacerlo.

 

Caso Práctico: Principio No Te Repitas

Vamos a escribir un ejemplo, buscamos si hay alguna manera de mejorarlo y lo refactorizamos.
Tenemos un simple class Report que recibe información y li imprime vía consola en un formato determinado.

class Report
{
   public function show(array $data)
   {
       echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
       echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
       echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       echo "Total: " . $data["total"] . "\n";
       echo "Average x day: " . floor($data["total"] / 365) . "\n";
       echo "Average x week: " . floor($data["total"] / 52) . "\n";
   }
}

Este código podría mejorarse pero por ahora nos es suficiente.
Tenemos una nueva petición para el report: poder guardarlo en un fichero. ¿Fácil no? Hagamos un copiar / pegar, unos pequeños cambios y en un par de minutos ya hemos terminado:

class Report
{
   public function show(array $data)
   {
       echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
       echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
       echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       echo "Total: " . $data["total"] . "\n";
       echo "Average x day: " . floor($data["total"] / 365) . "\n";
       echo "Average x week: " . floor($data["total"] / 52) . "\n";
       echo "Average x month: " . floor($data["total"] / 12) . "\n";
   }

   public function saveToFile(array $data)
   {
       $report = '';
       $report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
       $report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
       $report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       $report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
       $report .= "Average x day: " . floor($data["total"] / 365) . "\n";
       $report .= "Average x week: " . floor($data["total"] / 52) . "\n";
       $report .= "Average x month: " . floor($data["total"] / 12) . "\n";

       file_put_contents("./report.txt", $report);
   }
}

Espera, espera!! ¿De verdad hemos acabado aquí? Hemos hecho lo que nos han pedido, pero no acaba de estar del todo bien desde la parte técnica. Está lleno de repeticiones por todas partes ( aunque el código solo sean pocas líneas ). Encontramos WET por todas partes (Write Everything Twice – Escribe todo dos veces : contrario a lo que buscamos en DRY).


Hagamos un poco de refactor. Estos dos métodos prácticamente hacen lo mismo y la única diferencia está en el resultado final. Un buen inicio sería extraer el cuerpo en un método nuevo.
De esta forma tendremos una única fuente de verdad: el report solo se crea desde un único punto. Los demás métodos tendrán la responsabilidad única de decidir qué hacer con el report.

class Report
{
   public function show(array $data)
   {
       echo $this->createReport($data);
   }

   public function saveToFile(array $data)
   {
       file_put_contents("./report.txt", $this->createReport($data));
   }

   private function createReport(array $data): string
   {
       $report = '';
       $report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
       $report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
       $report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
       $report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
       $report .= "Average x day: " . floor($data["total"] / 365) . "\n";
       $report .= "Average x week: " . floor($data["total"] / 52) . "\n";
       $report .= "Average x month: " . floor($data["total"] / 12) . "\n";
       return $report;
   }
}

Bastante mejor ahora, ¿verdad? ¿Algo más? Hay unas pequeñas repeticiones que todavía podemos mejorar. Por ejemplo, hay las mismas transformaciones en el nombre del report y el producto:

$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";

Podemos extraer esta transformación en un nuevo método (o incluso mejor, a una librería con sus pruebas unitarias)

private function normalizeName($name): string
{
   return ucwords(strtolower($name));
}

Otra duplicación: el formato de la fecha.

$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";

Hagamos extracción de eso también:


private function formatDate($date): string
{
   return date("Y/m/d", $date);
}

Y por último: el cálculo de la media.


$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";

Aunque los cálculos no sean exactamanete los mismos, podemos hacer algo como esto:


private function calculateAverage(array $data, $period): string
{
   return floor($data["total"] / $period);
}

Por lo que el Report final tendrá la siguiente forma


class Report
{
   public function show(array $data)
   {
       echo $this->createReport($data);
   }

   public function saveToFile(array $data)
   {
       file_put_contents("./report.txt", $this->createReport($data));
   }

   private function createReport(array $data)
   {
       $report = '';
       $report .= "Report: " . $this->normalizeName($data["name"]) . "\n";
       $report .= "Product: " . $this->normalizeName($data["product"]) . "\n";
       $report .= "Start date: " . $this->formatDate($data["startDate"]) . "\n";
       $report .= "End date: " . $this->formatDate($data["endDate"]) . "\n";
       $report .= "Total: " . $data["total"] . "\n";
       $report .= "Average x day: " . $this->calculateAverage($data, 365) . "\n";
       $report .= "Average x week: " . $this->calculateAverage($data, 52) . "\n";
       $report .= "Average x month: " . $this->calculateAverage($data, 12) . "\n";
       return $report;
   }

   private function formatDate($date): string
   {
       return date("Y/m/d", $date);
   }

   private function calculateAverage(array $data, $period): string
   {
       return floor($data["total"] / $period);
   }

   private function normalizeName($name): string
   {
       return ucwords(strtolower($name));
   }
}

 

Regla de tres

Este ha sido un ejemplo básico, donde he intentado mostrar la importancia de evitar los duplicados y como mejorarlos. Obviamente, los duplicados no siempre van a ser fácil de reconocer. O para poder eliminarlos puede que sea más complejo y tener que aplicar algunos patrones de diseño, por ejemplo.
Una regla de oro en el refactoring es la Regla de tres. Repetir una vez el mismo código puede ser aceptable. Pero la tercera vez que utilizamos el mismo código, es señal inequívoca de que hay que refactorizar y solucionar la duplicación.

 

Conclusión: Principio No Te Repitas

  • Con todos estos pasos, hemos eliminado el WET de nuestro código
  • Si el report necesita un cambio, solamente se hará en un único punto. No nos hace falta aplicarlo en distintos lugares como hasta ahora.
  • Las pequeñas duplicaciones también las hemos eliminado. Los métodos contienen nombres significativos que explican lo que hacen. Mejor código.
  • Estos métodos los podemos extraer hacia alguna librería de ayuda con sus test unitarios.
  • Tengamos el principio DRY en mente la próxima vez que escribamos código.
  • No te olvides de la REGLA DE TRES

 

Si estás interesado en recibir más artículos sobre principio No te repitas, no te olvides de suscribirse a nuestro boletín mensual aquí.

Si este artículo sobre principio No te repitas te gustó, te puede interesar:

PHP funcional 

Tendencias en aplicaciónes móviles

Patrón MVP en iOS

Debugging con Charles Proxy en Android emulator

Por qué Kotlin?   

Integración Continua en iOS usando Fastlane y Jenkins  

Meetups de arquitectura de software  

MVPP en Android