Básicos de limpieza de código

En esta primera parte nos centraremos en conceptos básicos que nos ayudan a organizar y hacer más entendible el código. No buscan tanto hacer un rediseño del mismo, como mejorar su legibilidad y mantenibilidad. Al estar más ordenado y con los conceptos mejor identificados, será más fácil hacer cambios y corregir errores, pero también será más fácil descubrir patrones con los que propiciar cambios en el diseño.

Se podría decir que estos refactors que se proponen forman parte de la limpieza diaria. No deberían consumir tiempo y tendríamos que realizarlos en todo momento en que tengamos oportunidad y tenga sentido. No obstante, si el código es muy antiguo o no se ha hecho nunca, es posible que necesitemos un tiempo para hacer una limpieza inicial. En cualquier caso, es importante que no se convierta en una tarea pesada y que la abandones. Siempre es mejor hacer un refactor pequeño que no hacerlo nunca.

Cuando los comentarios confunden

En el que se trata de como gestionar los comentarios de un código, con especial atención a las situaciones en que el comentario más que ayudar nos confunde, así como criterios para decidir qué comentarios mantener y cuáles no.

Comentarios y documentación

Los lenguajes de programación incluyen la posibilidad de insertar comentarios en el código como forma de añadir documentación al mismo. Es decir: el objetivo de los comentarios es poner conocimiento cerca del lugar en el que puede ser necesario.

Los comentarios en el código parecen una buena idea y, probablemente, eran más útiles en otros tiempos, cuando la necesidad de economizar recursos y las limitaciones de los lenguajes de programación no nos permitían escribir un código lo bastante expresivo como para ser capaz de documentarse a sí mismo.

Así que, en este capítulo, intentaremos explicar qué comentarios nos sobran y por qué, y cuáles dejar.

Notas de la segunda edición

No hay grandes cambios en este capítulo. En general, se mantienen las mismas ideas que en la primera edición. Lo más significativo es que hemos añadido una sección con una heurística (las seis preguntas) que puede ser útil cuando tengas que decidir si añadir o mantener un comentario.

Por lo demás, este capítulo no es una diatriba general contra los comentarios o la documentación en el código, sino contra la documentación innecesaria, desactualizada o aquella que puede quedar fácilmente desactualizada.

Como norma general, la documentación debería estar lo más cerca posible del propio código. Esta podría ser una forma útil de organizar la documentación:

  • El código expresa claramente qué hace y cómo.
  • Los tests documentan cómo se usa el código, mediante ejemplos que se pueden ejecutar.
  • Se añaden comentarios cuando clarifican el porqué de ciertas decisiones.
  • El gestor de versiones documenta la historia del desarrollo.
  • El archivo README explica la naturaleza del proyecto, su relación con otros proyectos en su caso, y documenta el proceso de instalación, uso en local, desarrollo y testing.
  • Mejor aún si estos procesos están automatizados, mediante una herramienta tipo make o la equivalente para un ecosistema específico. Considérala un pipeline local.
  • El archivo README enlaza a documentos que puedan ser útiles, como how-to, configuración del IDE para el proyecto, o cualquier otro proceso que se considere oportuno, y se guardan en el mismo repositorio.
  • Las decisiones más abstractas sobre el código, como pueden ser decisiones sobre diseño, convenciones de código, decisiones sobre tecnologías, etc., se pueden documentar mediante Architecture Decision Records (ADR), que se guardarán también en el repositorio.
  • Los manuales de uso de la aplicación o el software pueden documentarse externamente, pero es conveniente enlazarlo en el README del proyecto, que actuaría como punto de entrada.

¿Por qué deberías eliminar comentarios?

Las principales razones para borrar comentarios son:

Suponen una dificultad añadida para leer el código. En muchos aspectos, los comentarios presentan una narrativa paralela a la del código y nuestro cerebro tiende a enfocarse en una de las dos. Si nos enfocamos en la de los comentarios, no estamos leyendo el código. Si nos enfocamos en la del código: ¿para qué queremos comentarios?

Los comentarios suponen una carga cognitiva. Incluso leyéndolos con el rabillo del ojo, los comentarios pueden suponer una carga cognitiva si, de algún modo, discrepan con lo que el código dice. Esto puede interrumpir tu flujo de lectura hasta que consigues aclarar si ese comentario tiene algún valor o no.

Pueden alargar innecesariamente un bloque de código. Idealmente, deberías poder leer un bloque de código en una sola pantalla. Los comentarios añaden líneas que podrían provocar que tengas que deslizar para ver todo el bloque. Son especialmente problemáticos los que están intercalados con las líneas de código.

Pueden mentir. Con el tiempo, si no se hace mantenimiento de los comentarios, estos acaban siendo mentirosos. Esto ocurre porque los cambios en el código no siempre son reflejados con cambios en los comentarios por lo que llegará un momento en que unos y otros no tengan nada que ver.

Refactor de comentarios

Lo básico

Simplemente, eliminamos los comentarios que no necesitamos. Es un refactor completamente seguro, ya que no afecta de ningún modo al código.

Reemplazar comentarios por mejores nombres

Eliminamos comentarios obvios, redundantes o innecesarios, cambiando el nombre de los símbolos que tratan de explicar.

Cambiar nombres es un refactor muy seguro, sobre todo con la ayuda de un buen IDE, que puede realizarlo automáticamente, y dentro de ámbitos limitados, como método o variables privadas.

Reemplazar comentarios por nuevas implementaciones

En algunos casos podríamos plantearnos mejorar el diseño de una parte del código porque al reflexionar sobre la necesidad de mantener un comentario nos damos cuenta de que es posible expresar la misma idea en el código.

Este tipo de refactor no encaja en la idea de esta serie sobre refactor cotidiano, pero plantea el modo en que los pequeños refactors del día a día van despejando el camino para refactors e incluso reescrituras de mayor alcance.

Comentarios redundantes

Los comentarios redundantes son aquellos que nos repiten lo que ya dice el código, por lo que podemos eliminarlos.

Por ejemplo:

1 // Class to represent a Book
2 class Book
3 {
4     //...
5 }

En serio, ¿qué nos aporta este comentario que no esté ya expresado?

1 class Book
2 {
3     //...
4 }

Los lenguajes fuertemente tipados, que soportan type hinting y/o return typing, nos ahorran toneladas de comentarios. Y de tests.

 1 class Book
 2 {
 3     /**
 4     * Creates a book with a title and an author
 5     *
 6     * @param Title $title
 7     * @param Author $author
 8     * @returns Book
 9     */
10     public static function create(Title $title, Author $author): Book
11     {
12         //
13     }
14 }

Los tipos de los parámetros y del objeto devuelto están explícitos en el código, por lo que es redundante que aparezcan como comentarios.

 1 class Book
 2 {
 3     /**
 4     * Creates a book with a title and an author
 5     */
 6     public static function create(Title $title, Author $author): Book
 7     {
 8         //
 9     }
10 }

Este tipo de comentarios tiene su razón de ser cuando el lenguaje no nos permite hacer explícitos los tipos.

Puedes eliminar los comentarios redundantes poniendo mejores nombres. Por ejemplo, en este caso en que utilizamos un constructor secundario:

 1 class Book
 2 {
 3     /**
 4     * Creates a book with a title and an author
 5     */
 6     public static function create(Title $title, Author $author): Book
 7     {
 8         //
 9     }
10 }

Con un nombre expresivo ya no necesitamos comentario:

1 class Book
2 {
3     public static function withTitleAndAuthor(Title $title, Author $aut\
4 hor): Book
5     {
6         //
7     }
8 }

O incluso más explícito:

1 class Book
2 {
3     public static function newWithTitleAndAuthor(Title $title, Author $\
4 author): Book
5     {
6         //
7     }
8 }

Y podemos usar el objeto así, lo cual documenta perfectamente lo que está pasando:

1 $newBook = Book::withTitleAndAuthor($title, $author);
1 $newBook = Book::newWithTitleAndAuthor($title, $author);

Si lo que estamos desarrollando es una librería que pueda utilizarse en múltiples proyectos, incluso que no sean nuestros, los comentarios que describen en detalle lo que hace el código pueden ser necesarios.

Comentarios mentirosos

Los comentarios mentirosos son aquellos que dicen algo distinto que el código. Y, por tanto, deben desaparecer.

¿De dónde vienen los comentarios mentirosos? Sencillamente, ha ocurrido que los comentarios se han quedado olvidados, sin mantenimiento, mientras que el código ha evolucionado. Por eso, cuando los lees hoy es posible que digan cosas que ya no valen para nada.

Mi experiencia personal con este tipo de comentarios cuando entro a un código nuevo suele ser bastante negativa. Si el comentario y el código difieren te encuentras con el problema de decidir a cuál de los dos hacer caso. Lo cierto es que el código manda porque es lo que realmente se está ejecutando y lo que está produciendo resultados, pero la existencia del comentario genera esa inquietud: ¿por qué el comentario dice una cosa y el código hace otra? La normal general sería borrar el comentario, pero es recomendable consultarlo primero.

Habitualmente, debería ser suficiente con comprobar si hay diferencias en la fecha en que se añadió el comentario y la fecha en la que se modificó el código. Si esta es posterior, es que tenemos un caso de comentario mentiroso por abandono y lo más adecuado sería borrarlo.

Este hecho debería bastar para que no añadas nuevos comentarios sin una buena razón. Tendemos a ignorar los comentarios triviales, de modo que cuando cambiamos el código nos despreocupamos de mantenerlos actualizados y acaban siendo mentirosos. Así que procuraremos dejar solo aquellos comentarios que nos importen realmente.

Si ya nos hemos librado de los comentarios redundantes, deberíamos contar solo con los que pueden aportar alguna información útil, así que nos toca examinarlos para asegurarnos de que no sean mentirosos. Y serán mentirosos si no nos cuentan lo mismo que cuenta el código.

Puede parecer un poco absurdo, pero al fin y al cabo los comentarios simplemente están ahí y no les prestamos mucha atención, salvo que sea la primera vez que nos movemos por cierto fragmento de código y tratamos de aprovechar cualquier información que nos parezca útil. Es entonces cuando descubrimos comentarios que pueden oscilar entre lo simplemente desactualizado y lo esperpéntico.

Así que, fuera con ellos. Algunos ejemplos:

To-dos olvidados. Las anotaciones To do seguramente hace meses que han dejado de tener sentido. Mienten en tanto que no tenemos ninguna referencia que les aporte significado.

¿De qué otro tipo de servicio estábamos hablando aquí hace tres meses? ¿Será que ya lo hemos cambiado?

1 // @todo we should use another kind of service here
2 
3 $service = new Service();
4 $service->execute();

Sería diferente si el comentario fuese mucho más preciso y detallado, de tal forma que indique con claridad el ámbito y plazo de la tarea pendiente. Algo así:

1 /**
2 @todo we should replace this service with the new implementation that s\
3 upport Kafka brokers when infra team finished the migration from SQS, s\
4 cheduled for 18/09/2023
5 */
6 
7 $service = new Service();
8 $service->execute();

En ese caso, el comentario hace explícitos unos detalles que definen con precisión los motivos, acciones y plazos. Ya puestas a añadir comentarios, que sean narrativos, detallados y bien útiles.

Comentarios olvidados. En algunos casos puede ocurrir que simplemente nos hayamos dejado comentarios olvidados. Por ejemplo, podríamos haber usado comentarios para definir las líneas básicas de un algoritmo, que es una técnica bien conocida, y ahí se habrían quedado. Todo ello también tiene que desaparecer:

 1 public function calculateFee(Request $dataToCalculate)
 2 {
 3     // Normalize amounts to same currency
 4     
 5     // ... code here
 6     
 7     // Perform initial calculation
 8     
 9     // ... more code here
10     
11     // Apply transormation
12     
13     // .., more code here
14 }

Comentarios para estructurar código. Claro que puede que el algoritmo sea lo bastante complejo como para que necesitemos describir sus diferentes partes. En este caso, el mejor refactor es extraer esas partes a métodos privados con nombres descriptivos, en lugar de usar comentarios:

 1 public function calculateFee(Request $dataToCalculate): Response
 2 {
 3     $this->normalizeAmountsToTheSameCurrency($dataToCalculate);
 4     $initialCalculation = $this->performInitialCalculation($dataToCalcu\
 5 late);
 6     $transformedResponse = $this->applyTransformation($initialCalculati\
 7 on);
 8     
 9     return $transformedResponse;gf
10 }

De este modo el código está estructurado y documentado.

Comentarios sobre valores válidos. Consideremos este código:

1 // Valid values: started, paused, running, terminated
2 public function updateStatus(string $newStatus): void
3 {
4     $this->checkValidStatus($newStatus);
5     $this->status = $newStatus;
6 }

El comentario delimita los valores aceptables para un parámetro, pero no fuerza ninguno de ellos. Eso tenemos que hacerlo mediante una cláusula de guarda. ¿Hay una forma mejor de hacerlo?

Por supuesto: utilizar un enumerable.

 1 class Status
 2 {
 3     private const STARTED = 'started';
 4     private const PAUSED = 'paused';
 5     private const RUNNING = 'running';
 6     private const TERMINATED = 'terminated';
 7     
 8     private $value;
 9     
10     private function __construct(string $status)
11     {
12         $this->checkValidStatus($status);
13         $this->status = $status;    
14     }
15     
16     public static function fromString(string $status): Status
17     {
18         return new self($status);
19     }
20     
21     public static function started(): Status
22     {
23         return new self(self::STARTED);
24     }
25     
26     //...
27 }

Lo que permite eliminar el comentario, a la vez que tener una implementación más limpia y coherente:

1 public function updateStatus(Status $newStatus): void
2 {
3     $this->status = $newStatus;
4 }

Código comentado

En alguna parte he escuchado o leído algo así como “código comentado: código borrado”. El código comentado debería desaparecer. Lo más seguro es que ya nadie se acuerde de por qué estaba ese código ahí, para empezar, y por qué sigue aunque sea escondido en un comentario.

Si es necesario recuperarlo (spoiler: no lo será) siempre nos queda el control de versiones.

A veces se puede usar la técnica de comentar un código para desactivarlo temporalmente. En ese caso, deberíamos explicar esa decisión también en el mismo comentario. Mucho mejor que eso es utilizar alguna técnica de feature flag. Existen librerías en todos los lenguajes para gestionar feature flags, pero en muchos casos podemos introducer alguna variable que sea fácil de cambiar:

1 if ($useNewCode === true) {
2     $this->newCode();
3 } else {
4     $this->oldCode();
5 }

Comentarios que podríamos conservar… o no

Comentarios que explican decisiones

Los buenos comentarios deberían explicar por qué tomamos alguna decisión que no podemos expresar mediante el propio código y que, por su naturaleza, podríamos considerar como independiente de la implementación concreta que el código realiza. Es decir, no deberíamos escribir comentarios que expliquen cómo es el código, que es algo que ya podemos ver, sino que expliquen por qué es así.

Lo normal es que estos comentarios sean pocos, pero relevantes, lo cual los pone en una buena situación para realizar un mantenimiento activo de los mismos.

Obviamente, corremos el riesgo de que los comentarios se hagan obsoletos si olvidamos actualizarlos cuando sea necesario. Por eso la importancia de que no estén “acoplados” a la implementación en código.

Un ejemplo de comentario relevante podría ser este:

1 // We apply taxes following the procedure stated in law RD 2018/09
2 public function applyTaxes(Money $totalAmountBeforeTaxes): Money
3 {
4     //... some code here
5 }

Este comentario es completamente independiente del código e indica una información importante que no podríamos expresar con él. Si en un momento dado cambia la legislación y debemos aplicar otra normativa, podemos cambiar el comentario.

Aunque, a decir verdad, podríamos llegar a expresarlo en código. A grandes rasgos:

 1 interface Taxes
 2 {
 3     public function apply(Money $amountBeforeTaxes): Money;
 4 }
 5 
 6 class RD201809Taxes implements Taxes
 7 {
 8     public function apply(Money $amountBeforeTaxes): Money
 9     {
10         // ... some code here
11     }
12 }
13 
14 
15 class RD201821Taxes implements Taxes
16 {
17     public function apply(Money $amountBeforeTaxes): Money
18     {
19         // ... some code here
20     }
21 }

Dudas razonables

Comentarios para el IDE

En aquellos lenguajes en los que el análisis estático por parte del IDE no pueda interpretar algunas cosas, añadir comentarios en forma de anotaciones puede suponer una ayuda para el IDE. En algunos casos, gracias a eso nos avisa de problemas potenciales antes de integrar los cambios.

No debería ser una práctica común, pero es un compromiso aceptable. Por ejemplo, en PHP era frecuente indicar el tipo de las propiedades de los objetos y otras variables con comentarios, ya que el lenguaje no permitía hacerlo en código.

1 class Status
2 {
3     /** string **/
4     private $value;
5 }

Esto se introdujo en la versión 7.4:

1 class Status
2 {
3     private string $value;
4 }

En otros lenguajes, estas características estaban presentes desde mucho antes.

Las seis preguntas

El framework de las seis preguntas se utiliza en algunas disciplinas para determinar si una fuente proporciona información completa. Estas preguntas se pueden usar para decidir qué comentar en un código y qué no es necesario:

  • ¿Cuándo se ha escrito el código?: Esta información la encontramos fácilmente en el sistema de control de versiones. No hay que añadirla como comentario. El único caso en que se me ocurre que podría ser útil es cuando mudamos un repositorio a un servidor diferente, ya que se puede perder la información.
  • ¿Quién ha escrito el código?: Aplica lo mismo que en la pregunta anterior, es información que nos proporciona el sistema de control de versiones, de una forma mucho más precisa.
  • ¿Dónde está el código?: Aplicado a la paquetización del código, básicamente es una información que o bien se declara de forma explícita, o bien el lenguaje se encarga de reportarnos en caso de errores. Por tanto, tampoco parece necesario establecerlo en un comentario.
  • ¿Qué hace el código?: La respuesta corta es que el código ya dice lo que hace, pero con frecuencia eso no queda tan claro porque los nombres están mal escogidos o la estructura del código lleva a confusión. Eso podría llevarnos a plantear la necesidad de indicarlo en un comentario. Pero antes de ellos, lo apropiado sería reflexionar sobre cómo explicitar la intención de ese fragmento de código usando un buen nombre. Y, de todos modos, la mejor forma de documentar lo que hace un código es mediante un test.
  • ¿Cómo hace el código lo que hace?: De nuevo, una vez que sabemos lo que hace un código, el cómo debería ser el código en sí. Ahora bien, hay algunos casos en los que es recomendable añadir comentarios. Uno de esos casos es el uso de algoritmos bien conocidos, que tienen nombre. En esa situación, es muy buena idea hacerlo explícito. Otro caso podría ser el de documentar distintos pasos en un algoritmo, aunque para ello suele ser mejor extraerlos a sus propios métodos.
  • ¿Por qué hace el código lo que hace?: Finalmente, esta es una pregunta que solo podemos contestar nosotras: las personas responsables de ese conocimiento. Y esa explicación debe aparecer como comentario.

Resumen del capítulo

Los comentarios en el código tienen una utilidad limitada y, con frecuencia, se vuelven mentirosos y no resultan de ayuda para comprender lo que nuestro código hace, pudiendo incluso llevarnos a confusión si les hacemos caso.

Si introduces un comentario, debes responsabilizarte de su ciclo de vida: actualizarlo cuando cambie el código al que hace referencia. Borrarlo si ya no sirve de nada.

En lugar de usar comentarios es preferible trabajar en mejores nombres para los símbolos (variables, constantes, clases, métodos, funciones…), estructurar mejor el código en funciones o métodos que expresen su intención.

Si necesitamos documentar cómo funciona algo y cómo usarlo, es mucho mejor introducir tests, los cuales proporcionan una documentación viva.

Por otro lado, los comentarios que sí pueden permanecer suelen referirse a aspectos que no podemos expresar fácilmente con código, como puede ser explicar los motivos para hacer algo de una forma concreta. Debes asegurarte de mantenerlos al día.

El nombre de la cosa

En el que Adso y Guillermo… ejem, en el que discutimos sobre la necesidad de poner los nombres adecuados a las cosas del código, a fin de que se entienda qué demonios pasaba en aquel momento por nuestra cabeza, o la de la autora del código que tenemos que intervenir.

Probablemente en ningún lugar como el código los nombres configuran la realidad. Escribir código implica establecer decenas de nombres cada día, para identificar conceptos y procesos. Una mala elección de nombre puede condicionar nuestra forma de ver un problema de negocio. Un nombre ambiguo puede llevarnos a entrar en un callejón sin salida, ahora o en un futuro no muy lejano. Pero un nombre bien escogido puede ahorrarnos tiempo, dinero y dificultades.

Notas de la segunda edición

En este capítulo hemos cambiado los ejemplos para que los nombres originales sean mucho menos expresivos. De este modo, se entiende mejor la necesidad del refactor, y también se entiende mejor lo que hace la clase Calculator que hemos usado como ejercicio.

Símbolos con nombres

Un trozo de código debería poder leerse como una especie de narrativa, en la cual cada palabra expresase de forma unívoca un significado. También de forma ubicua y coherente, es decir, que el mismo símbolo debería representar el mismo concepto en todas partes del código.

¿Cuándo refactorizar nombres?

La regla de oro es muy sencilla: cada vez que al leer una línea de código tenemos que pararnos a pensar qué está diciendo lo más probable sea que debamos cambiar algún nombre.

Este es un ejemplo de un código en el que nos encontramos con unos cuantos problemas de nombres, algunos son evidentes y otros no tanto:

 1 class Item {
 2     private $val;
 3 
 4     public function __construct($val) {
 5         $this->val = $val;
 6     }
 7 
 8     public function getVal() {
 9         return $this->val;
10     }
11 }
12 
13 class Calculator {
14     private $rate1;
15     private $rate2;
16 
17     public function __construct($rate1, $rate2) {
18         $this->rate1 = $rate1;
19         $this->rate2 = $rate2;
20     }
21 
22     public function calc($item) {
23         $val = $item->getVal();
24         $val = $this->applyRate2($val);
25         $val = $this->applyRate1($val);
26         return $val;
27     }
28 
29     private function applyRate2($val) {
30         return $val * (1 + $this->rate2);
31     }
32 
33     private function applyRate1($val) {
34         return $val * (1 - $this->rate1);
35     }
36 }

Por supuesto, en este ejemplo hay algunos errores más aparte de los nombres. Pero hoy solo nos ocuparemos de estos. Vamos por partes.

Nombres demasiado genéricos

Los nombres demasiado genéricos requieren el esfuerzo de interpretar el caso concreto en que se están aplicando. Además, en un plano más práctico, resulta difícil localizar una aparición específica del mismo que tenga el significado deseado.

¿De dónde vienen los nombres demasiado genéricos? Normalmente, vienen de estadios iniciales del código, en los que probablemente bastaba con ese término genérico para designar un concepto. Con el tiempo, ese concepto evoluciona y se ramifica a medida que el conocimiento de negocio avanza, pero el código puede que no lo haya hecho al mismo ritmo, con lo que llega un momento en que este no es reflejo del conocimiento actual que tenemos del negocio.

Calculate… what? Exactamente, ¿qué estamos calculando aquí? El código no lo refleja. Podría ocurrir, por ejemplo, que $rate1 fuese algún tipo de descuento, $rate2 podría ser una comisión o impuestos y $val parece claro que es algo así como el precio de tarifa de algún producto o servicio, sea lo que sea que vende esta empresa. Es muy posible que este método lo que haga sea calcular el precio final para el consumidor del producto. ¿Por qué no declararlo de forma explícita?

 1 class Calculator {
 2     private $rate1;
 3     private $rate2;
 4 
 5     public function __construct($rate1, $rate2) {
 6         $this->rate1 = $rate1;
 7         $this->rate2 = $rate2;
 8     }
 9 
10     public function finalPrice($item) {
11         $val = $item->getVal();
12         $val = $this->applyRate2($val);
13         $val = $this->applyRate1($val);
14         return $val;
15     }
16 
17     private function applyRate2($val) {
18         return $val * (1 + $this->rate2);
19     }
20 
21     private function applyRate1($val) {
22         return $val * (1 - $this->rate1);
23     }
24 }

Vamos a revisar los distintos nombres que se están usando en el código para representar los conceptos que se manejan en esta calculadora de precios. Puesto que tenemos bastante claro que $val es el precio del producto, podemos hacerlo explícito.

 1 class Calculator {
 2     private $rate1;
 3     private $rate2;
 4 
 5     public function __construct($rate1, $rate2) {
 6         $this->rate1 = $rate1;
 7         $this->rate2 = $rate2;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->getVal();
12         $price = $this->applyRate2($price);
13         $price = $this->applyRate1($price);
14         return $price;
15     }
16 
17     private function applyRate2($price) {
18         return $price * (1 + $this->rate2);
19     }
20 
21     private function applyRate1($price) {
22         return $price * (1 - $this->rate1);
23     }
24 }

La clase Item, que representa el producto o servicio que estamos vendiendo nos proporciona ese precio base e, igualmente, deberíamos hacerlo explícito.

 1 class Item {
 2     private $price;
 3 
 4     public function __construct($price) {
 5         $this->basePrice = $price;
 6     }
 7 
 8     public function basePrice() {
 9         return $this->basePrice;
10     }
11 }

Con estos cambios de nombres, debería haber quedado más claro qué es lo que está pasando.

 1 class Calculator {
 2     private $rate1;
 3     private $rate2;
 4 
 5     public function __construct($rate1, $rate2) {
 6         $this->rate1 = $rate1;
 7         $this->rate2 = $rate2;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->basePrice();
12         $price = $this->applyRate2($price);
13         $price = $this->applyRate1($price);
14         return $price;
15     }
16 
17     private function applyRate2($price) {
18         return $price * (1 + $this->rate2);
19     }
20 
21     private function applyRate1($price) {
22         return $price * (1 - $this->rate1);
23     }
24 }

Nombres ambiguos

Hay dos propiedades en la calculadora que tienen el mismo nombre: $rate1 y $rate2.

Técnicamente, son correctos, ya que $rate nos sugiere un porcentaje o proporción, algo que podemos confirmar al leer los métodos en los que se aplican. Pero, ¿qué concepto de negocio representan?

Sabemos que se aplican dos, pero no sabemos qué representan. Uno de ellos se resta y el otro se suma. El que se resta, puede ser un descuento, mientras que el que se suma, podría tratarse de un impuesto o una comisión. Debería ser obvio que necesitamos clarificarlo.

Tras hablarlo con negocio, hemos llegado a la conclusión de $rate1 representa un descuento y $rate2 un impuesto. Lo adecuado, en este caso, será poner nombres explícitos.

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->basePrice();
12         $price = $this->applyRate2($price);
13         $price = $this->applyRate1($price);
14         return $price;
15     }
16 
17     private function applyRate2($price) {
18         return $price * (1 + $this->tax);
19     }
20 
21     private function applyRate1($price) {
22         return $price * (1 - $this->discount);
23     }
24 }

Con este refactor ya hemos ganado mucho en expresividad, pero el término Rate se utiliza en varios nombres compuestos, por lo que no hemos terminado aún. El siguiente paso sería cambiar applyRate1 y applyRate2 por algo más descriptivo:

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->basePrice();
12         $price = $this->applyTax($price);
13         $price = $this->applyDiscount($price);
14         return $price;
15     }
16 
17     private function applyTax($price) {
18         return $price * (1 + $this->tax);
19     }
20 
21     private function applyDiscount($price) {
22         return $price * (1 - $this->discount);
23     }
24 }

Aplicando este refactor hemos conseguido mucha más claridad y legibilidad. Incluso puede que hayamos hecho aflorar un bug. Si te fijas, se aplican los descuentos tras aplicar los impuestos. En muchos países, los impuestos se aplican sobre el precio final, por lo que deberíamos cambiar el orden de aplicación de los métodos applyTax y applyDiscount.

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->basePrice();
12         $price = $this->applyDiscount($price);
13         $price = $this->applyTax($price);
14         return $price;
15     }
16 
17     private function applyTax($price) {
18         return $price * (1 + $this->tax);
19     }
20 
21     private function applyDiscount($price) {
22         return $price * (1 - $this->discount);
23     }
24 }

Esto es muy interesante porque nos demuestra como los nombres ambiguos puede llevarnos a errores de lógica. Al emplear el mismo término $rate para referirnos a dos conceptos completamente distintos, hemos podido confundirnos y aplicar los descuentos antes de los impuestos, lo que podría llevar a problemas legales o a pérdidas económicas. Ciertamente, sería posible prevenirlo con un buen test, que nos habría indicado el error cometido, pero, y si no tenemos un test que cubra ese caso concreto, ¿cómo lo detectaríamos?

Pero es que, además, al eliminar la ambigüedad de los nombres, reducimos la dificultad y el tiempo que nos llevaría interpretar el código. No solo para nosotras, sino también para cualquier persona que pueda tener que trabajar con él en el futuro.

Nombres reutilizados en el mismo scope

Aunque ahora tenemos el código en un estado mucho mejor, todavía tenemos un aspecto que no está realmente bien. La variable $price es actualizada constantemente y, no solo cambia de valor, sino que cambia de significado. En un momento dado, $price es el precio base del producto, en otro es el precio con el descuento aplicado y en otro es el precio con el impuesto aplicado.

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $item->basePrice();
12         $price = $this->applyDiscount($price);
13         $price = $this->applyTax($price);
14         return $price;
15     }
16 
17     private function applyTax($price) {
18         return $price * (1 + $this->tax);
19     }
20 
21     private function applyDiscount($price) {
22         return $price * (1 - $this->discount);
23     }
24 }

El principal inconveniente de esta forma de programar es que tiene un coste de cambio alto en el futuro. Si hubiese que introducir algún paso nuevo en el cálculo del precio final, habría que modificar todos los lugares en los que se actualiza la variable $price. Además, el código es más difícil de comprender, ya que no es fácil saber en qué estado se encuentra $price en cada momento.

Podrías argumentar que se trata de una variable temporal y de que, en último término, lo que buscamos es el resultado final de todas las transformaciones. Por ejemplo, podríamos hacer algo así, y ni siquiera necesitaríamos la variable $price:

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $price = $this->applyTax(
12             $this->applyDiscount(
13                 $item->basePrice()
14             )
15         );
16         return $price;
17     }
18 
19     private function applyTax($price) {
20         return $price * (1 + $this->tax);
21     }
22 
23     private function applyDiscount($price) {
24         return $price * (1 - $this->discount);
25     }
26 }

Bien, esta solución es interesante, pero a primera vista ya parece más difícil de leer. No solo eso, al igual que la solución inicial nos vamos a encontrar con problemas en caso de que en algún momento necesitemos introducir un cambio.

De entrada, sería más sencillo bautizar cada paso del cálculo con un nombre que refleje su significado. Por ejemplo:

 1 class Calculator {
 2     private $discount;
 3     private $tax;
 4 
 5     public function __construct($discount, $tax) {
 6         $this->discount = $discount;
 7         $this->tax = $tax;
 8     }
 9 
10     public function finalPrice($item) {
11         $basePrice = $item->basePrice();
12         $discountedPrice = $this->applyDiscount($basePrice);
13         $priceAfterTax = $this->applyTax($discountedPrice);
14         return $priceAfterTax;
15     }
16 
17     private function applyTax($price) {
18         return $price * (1 + $this->tax);
19     }
20 
21     private function applyDiscount($price) {
22         return $price * (1 - $this->discount);
23     }
24 }

Más allá de los nombres: cuando aparecen conceptos

Por otro lado, este tipo de patrones en los que vamos cambiando el valor de una variable con cálculos que la toman como argumento de entrada, nos debería sugerir la presencia de un concepto que estaría mejor representado con un objeto. Como podemos ver, todas las operaciones se realizan sobre un precio, por lo que podríamos encapsularlo en un objeto Price. Y una vez que tenemos un objeto, lo más adecuado sería que este se encargase de aplicar los descuentos y los impuestos.

 1 class Price {
 2     private $amount;
 3 
 4     public function __construct($amount) {
 5         $this->amount = $amount;
 6     }
 7 
 8     public function applyTax($tax) {
 9         $this->amount *= (1 + $tax);
10     }
11 
12     public function applyDiscount($discount) {
13         $this->amount *= (1 - $discount);
14     }
15 
16     public function->amount() {
17         return $this->amount;
18     }
19 }
20 
21 class Calculator {
22     private $discount;
23     private $tax;
24 
25     public function __construct($discount, $tax) {
26         $this->discount = $discount;
27         $this->tax = $tax;
28     }
29 
30     public function finalPrice($item) {
31         $price = new Price($item->basePrice());
32         $price->applyDiscount($this->discount);
33         $price->applyTax($this->tax);
34         return $price->amount();
35     }
36 }

Este refactor que acabamos de mostrar va más allá del alcance de este capítulo, pero nos ha servido para mostrar que cuando mejoramos los nombres, es muy fácil que afloren conceptos interesantes que no teníamos bien representados con valores primitivos. En capítulos posteriores examinaremos cómo podemos introducir estos conceptos en nuestro código.

Tipo de palabra inadecuada

Los símbolos que, de algún modo, contradicen el concepto que representan son más difíciles de procesar, generalmente porque provocan una expectativa que no se cumple y, por tanto, debemos reevaluar lo que estamos leyendo. Por ejemplo:

  • Una acción debería representarse siempre mediante un verbo.
  • Un concepto, mediante un sustantivo.

A su vez, nunca nos sobran los adjetivos para precisar el significado del sustantivo, por lo que los nombres compuestos nos ayudan a representar con mayor precisión las cosas.

Volvamos al ejemplo. Calculator parece un buen nombre. PriceCalculator sería aún mejor, ya que hace explícito el hecho de que calcula precios. Es un sustantivo, por lo que se deduce que es un actor que hace algo. Veámosla como interface:

1 interface PriceCalculator {
2     public finalPrice(Item $item): float;
3 }

Obviamente, este refactor es un poco más arriesgado. Vamos a tocar una interfaz pública, pero también es verdad que con los IDE modernos este tipo de cambios es razonablemente seguro.

finalPrice es un sustantivo, pero en realidad ¿no representa una acción? ¿No sería mejor calculateFinalPrice?

1 interface PriceCalculator {
2     public calculateFinalPrice(Item $item): float;
3 }

Por un lado, es cierto que parece más imperativo. Estamos diciendo algo así como: “Calculadora: calcula el precio final”. No deja lugar a dudas sobre lo que hace. En el lado negativo, resulta un nombre redundante, a la par que largo.

Pero antes… Volvamos un momento a la clase. PriceCalculator, ¿es un actor o una acción? A veces tendemos a ver los objetos como representaciones de objetos del mundo real. Sin embargo, podemos representar acciones y otros conceptos con objetos en el código. Esta forma de verlo puede cambiar por completo nuestra manera de hacer las cosas.

Supongamos entonces, que consideramos que PriceCalculator no es una cosa, sino una acción:

1 interface CalculatePrice {
2     public calculateFinalPrice(Product $product): float;
3 }

Tal y como está ahora, expresar ciertas cosas resulta extraño:

1 $calculatePrice = new CalculatePrice();
2 
3 $calculatePrice->calculateFinalPrice($product);

Pero podemos imaginarlo de otra forma mucho más fluida:

1 $calculatePrice = new CalculatePrice();
2 
3 $calculatePrice->finalForProduct($product);

Lo que nos deja con esta interfaz:

1 interface CalculatePrice {
2     public finalForProduct(Product $product): float;
3 }

Este cambio de nombre resulta interesante, pero también tenemos que valorarlo en su contexto. Que los objetos tengan nombres de acciones puede ser muy válido para representar casos de uso, pero no tanto para representar entidades de negocio. En este caso, si lo entendemos como una entidad de o un proceso de negocio, PriceCalculator es un nombre mucho más adecuado.

Números mágicos

A veces no se trata estrictamente de refactorizar nombres, sino de bautizar elementos que están presentes en nuestro código en forma de valores abstractos que tienen un valor de negocio que no ha sido hecho explícito.

Poniéndoles un nombre, lo hacemos. Lo que antes era:

1 $vatAmount = $amountBeforeTaxes * .21;

En la línea anterior, .21 es un número mágico. No sabemos qué significa, pero podemos intuir que se trata de un impuesto y deberíamos hacerlo explícito.

Después:

1 const VAT_RATE = .21;
2 $vatAmount = $amountBeforeTaxes * VAT_RATE;

Convertir estos valores en constantes con nombre hace que su significado de negocio esté presente, sin tener que preocuparse de interpretarlo. Además, esto los hace reutilizables a lo largo de todo el código, lo que añade un plus de coherencia.

Así que, cada vez que encuentres uno de estos valores, hazte un favor y reemplázalo por una constante. Por ejemplo, los naturalmente ilegibles patrones de expresiones regulares:

1 $isValidNif = preg_match('/^[0-9XYZ]\d{7}[^\dUIOÑ]$/', $nif);
2 
3 // vs
4 
5 $isValidNif = preg_match(VALID_NIF_PATTERN, $nif);

O los patrones de formato para todo tipo de mensajes:

1 $mensaje = sprintf('¿Enviar un mensaje a %s en la dirección %s?', $user\
2 ->username(), $user->email());
3 
4 $mensaje = sprintf(CONFIRM_SEND_EMAIL_MESSAGE, $user->username(), $user\
5 ->email());

Nombres técnicos

Personalmente, me gustan poco los nombres técnicos formando parte de los nombres de variables, clases, interfaces, etc. De hecho, creo que en muchas ocasiones condicionan tanto el naming, que favorecen la creación de malos nombres.

Ya he hablado del problema de entender que los objetos en programación tienen que ser representaciones de objetos del mundo real. Esa forma de pensar nos lleva a ver todos los objetos como actores que hacen algo, cuando muchas veces son acciones.

En ocasiones, es verdad que tenemos que representar ciertas operaciones técnicas, que no todo va a ser negocio, pero eso no quiere decir que no hagamos las cosas de una manera elegante. Por ejemplo:

 1 interface BookTransformer
 2 {
 3     public function transformToJson(Book $book): string;
 4     public function transformFromJson(string $bookDto): Book;
 5 }
 6 
 7 // vs
 8 
 9 interface TransformBook
10 {
11     public function toJson(Book $book): string;
12     public function fromJson(string $bookDto): Book;
13 }

En cambio, en el dominio me choca ver cosas como:

1 class BookWasPrintedEvent implements DomainEvent
2 {
3 }
4 
5 // vs
6 
7 class BookWasPrinted implements DomainEvent
8 {
9 }

En este ejemplo, el uso del verbo en pasado debería ser suficiente para entender de un vistazo que está hablando de un evento, que no es otra cosa que un mensaje que indica que algo interesante ha ocurrido.

Es cierto que incluir algunos apellidos técnicos a nuestros nombres puede ayudarnos a localizar cosas en el IDE. Pero hay que recordar que no programamos para un IDE.

Refactor de nombres

En general, gracias a las capacidades de refactor de los IDE o incluso del Buscar/Reemplazar en proyectos, realizar refactors de nombres es bastante seguro.

Variables locales en métodos y funciones. Cambiarlas no supone ningún problema, pues no afectan a nada que ocurra fuera de su ámbito.

Propiedades y métodos privados en clases. Tampoco suponen ningún problema al no afectar a nada externo a la clase.

Interfaces públicas. Aunque es más delicado, los IDE modernos deberían ayudarnos a realizarlos sin riesgos importantes. La mayor dificultad me la he encontrado al cambiar nombres de clases, puesto que el IDE aunque localiza y cambia correctamente sus usos, no siempre identifica objetos relacionados, como los tests.

El coste de un mal nombre

Imaginemos un sistema de gestión de bibliotecas que, inicialmente, se creó para gestionar libros. Simplificando muchísimo, aquí tenemos un concepto clave del negocio:

1 class Book
2 {
3     private $id;
4     private $title;
5     private $author;
6     private $editor;
7     private $year;
8     private $city;
9 }

Con el tiempo la biblioteca pasó a gestionar revistas. Las revistas tienen número, pero tal vez en su momento se pensó que no sería necesario desarrollar una especialización:

 1 class Book
 2 {
 3     private $id;
 4     private $title;
 5     private $author;
 6     private $editor;
 7     private $year;
 8     private $city;
 9     private $issue;
10 }

Y aquí comienza un desastre que solo se detecta mucho tiempo después y que puede suponer una sangría, quizá lenta pero constante, de tiempo, recursos y, en último término, dinero para los equipos y empresas.

La modificación de la clase Book hizo que esta pasara a representar dos conceptos distintos, pero quizá se consideró que era una ambigüedad manejable: un compromiso aceptable.

Claro que la biblioteca siguió evolucionando y con el avance tecnológico comenzó a introducir nuevos tipos de objetos, como CD, DVD, libros electrónicos, y un largo etcétera. En este punto, el conocimiento que maneja negocio y su representación en el código se han alejado tanto que el código se ha convertido en una pesadilla: ¿cómo sabemos si Book se refiere a un libro físico, a uno electrónico, a una película en DVD, a un juego en CD? Solo podemos saberlo examinando el contenido de cada objeto Book. Es decir: el código nos está obligando a pararnos a pensar para entenderlo. Necesitamos refactorizar y reescribir.

Es cierto que, dejando aparte el contenido, todos los objetos culturales conservados en una biblioteca comparten ese carácter de objeto cultural o soporte de contenidos. CulturalObject se nos antoja un nombre demasiado forzado, pero Media resulta bastante manejable:

1 class Media
2 {
3     private $id;
4     private $signature;
5     private $registeredSince;
6     private $status;
7 }

Media representaría a los soportes de contenidos archivados en la biblioteca y que contendría propiedades como un número de registro (id), la signatura topográfica (que nos comunica su ubicación física) y otros detalles relacionados con la actividad de archivo, préstamo, etcétera.

Pero esa clase tendría especializaciones que representan tipos de medios específicos, con sus propiedades y comportamientos propios.

 1 class Book extends Media
 2 {
 3 }
 4 
 5 class Review extends Media
 6 {
 7 }
 8 
 9 class ElectronicBook extends Media
10 {
11 }
12 
13 class Movie extends Media
14 {
15 } 

Podríamos desarrollar más el conocimiento de negocio en el código, añadiendo interfaces. Por ejemplo, la gestión del préstamo:

1 interface Lendable
2 {
3     public function lend(User $user): void;
4     public function return(DateTimeInterface $date): void;
5 }

Pero el resumen es que el hecho de no haber ido reflejando la evolución del conocimiento del negocio en el código nos lleva a tener un sobre-coste en forma de:

  • El tiempo y recursos necesarios para actualizar el desarrollo a través de reescrituras.
  • El tiempo y recursos necesarios para mantener el software cuando surgen problemas derivados de la mala representación del conocimiento.
  • Las pérdidas por no ingresos debidos a la dificultad del software de adaptarse a las necesidades cambiantes del negocio.

Por esto, preocúpate por poner buenos nombres y mantenerlos al día. Va en ello tu salario.

Resumen del capítulo

Los nombres de los diversos objetos y mensajes que viven en un programa deberían ser precisos y reflejar el lenguaje del dominio que se trate. Un buen nombre es aquel que nos permite entender de un vistazo qué hace un objeto o qué representa un mensaje. Un mal nombre, por el contrario, nos obliga a pararnos a pensar y a interpretar lo que estamos leyendo.

Deja atrás lo primitivo

En el que se habla de que si estamos siguiendo un paradigma orientado a objetos, todo debería ser un objeto. Sí, todo.

Notas de la segunda edición

Este capítulo necesitaba una fuerte reescritura, dado que estaba muy orientado a Value Objects, que es un concepto muy ligado a una metodología concreta de desarrollo. En general, encapsular comportamiento y datos en objetos es una buena idea, siempre y cuando representen algún tipo de concepto significativo.

Otro cambio destacable es que hemos incluído aquí el contenido correspondiente a los tipos Enumerables. Al revisar ese capítulo nos hemos dado cuenta de que en realidad el capítulo de Enumerables estaba hablando del patrón State, que hemos movido a un capítulo dedicado.

Está lleno de objetos

Todos los lenguajes de programación vienen de serie con un conjunto de tipos de datos básicos que denominamos primitivos. En algún caso también se les llama escalares, cuando el lenguaje no los implementa nativamente como objetos: boolean, integer, float o string, entre otros, que utilizamos para representar cosas y operar con ellas. La parte mala es que se trata de tipos de datos muy genéricos y, a veces, necesitaríamos algo que aporte más significado y también más restricciones.

Los lenguajes orientados a objetos, en particular, ofrecen una estructura nativa de datos que permite encapsular primitivos y comportamiento en objetos. De este modo, podemos representar conceptos significativos en distintos dominios y niveles de abstracción. En algunos lenguajes también existe el tipo Struct, con el que podemos representar estructuras de datos simples, pero que no tienen comportamiento. Me voy a permitir hacer una clasificación de diversos tipos de objetos que podríamos tener en una aplicación:

  • Data Transfer Object (DTO): son objetos que se utilizan para transferir datos entre subsistemas de una aplicación. No tienen comportamiento y suelen ser muy simples, con propiedades públicas y sin métodos. Sus propiedades son tipos primitivos. Su ventaja es que son fáciles de serializar y deserializar. Por otro lado, podríamos incluir en esta categoría cualquier tipo de objeto que definimos con la única función de agrupar datos de forma arbitraria para mover información de un sitio a otro.
  • Tipos: son objetos tienen la función de suplementar el sistema de tipos del lenguaje para añadir algunas restricciones y aportar significado a los datos. Imagina, por ejemplo, que quieres tener cosas como NotEmptyString, PositiveNumber y similares. Estos tipos no representan conceptos de un dominio específico, sino que son simplemente tipos de datos que añaden restricciones a los primitivos. Estos objetos tendrían un comportamiento fundamentalmente técnico y genérico.
  • Objetos: en general, cualquier objeto que encapsula primitivos y comportamiento para representar algún concepto significativo del contexto en que se usa. La palabra dominio sería más correcta, pero está muy contaminada por el uso que se le da en DDD, por lo que prefiero evitarla. Por poner un ejemplo, la configuración de una base de datos podría representarse mediante un objeto DatabaseConfiguration, o un objeto File para representar un archivo en un sistema de almacenamiento, y un largo etcétera de casos. Pero, por supuesto, usaríamos objetos para representar conceptos de negocio, como Customer, Product, Order, etc.

En Domain Driven Design se habla de Value Objects y Entities. Ambos representan conceptos importantes del Dominio de Negocio de una aplicación. La principal diferencia es que los Value Objects nos interesan por su valor y son inmutables, mientras que las Entities nos interesan por su identidad y mutan a lo largo de su ciclo de vida.

El problema de los tipos primitivos

Validación. Cuando queremos representar algún concepto en código inicialmente recurrimos a los tipos disponibles. Pensemos, por ejemplo, en un email, el cual usamos frecuentemente como nombre de usuario porque es único y fácil de recordar. Puesto que lo podemos representar con un tipo string, es habitual encontrarnos con código como este:

1 $username = "user@example.com";

El problema es que un email no es un string cualquiera. Tiene una serie de restricciones que no se cumplen en un simple string. Por ejemplo, tiene que tener un formato concreto, con una longitud máxima, incluir el símbolo @, al menos un nombre de dominio, etc. Si queremos validar un email, tendremos que hacerlo manualmente. Esta sería una estrategia típica en PHP:

1 function isValidEmail(string $email): bool
2 {
3     return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
4 }

En cualquier caso, como $username es una variable, sería fácil que en cualquier momento cambie. En consecuencia nunca vamos a poder tener la seguridad de que $username es un email válido fuera del scope en que se haya validado, por lo que tenemos que repetir esa validación siempre.

Conceptos compuestos. Es muy frecuente que un concepto único se tenga que modelar con varios elementos. Así, por ejemplo, el nombre de una persona suele constar de nombre de pila y apellidos, para lo que podrían usarse dos strings:

1 $firstName = "Pepa";
2 $lastName = "Pérez García";

El problema obvio es que tenemos que tener esto en cuenta constantemente y mover estas variables juntas a todas partes:

1 function createFullName(string $firstName, string $lastName): string
2 {
3     return $firstName . ' ' . $lastName;
4 }

Lo mismo ocurre aquí, que siempre tendremos que pasar ambos datos:

 1 class Customer
 2 {
 3     private string $id;
 4     private string $name;
 5     private string $lastName;
 6     
 7     public function __construct(string $id, string $name, string $lastN\
 8 ame) {
 9         $this->id = $id;
10         $this->name = $name;
11         $this->lastName = $lastName;
12     }
13 }

La necesidad de mantener juntos un conjunto de datos da lugar a un code smell llamado Data Clump. Este tipo de diseños son costosos de mantener y cambiar, ya que es fácil olvidar todos los elementos que tienen que mantenerse juntos y tenemos que hacer seguimiento de ellos en todos los rincones del código.

Primitive Obsession

En último término, usar tipos primitivos para representar cualquier tipo de concepto provoca el code smell llamado Primitive Obsession. Aunque en muchos contextos es perfectamente válido usar tipos primitivos, cuando estamos modelando un dominio complejo es preferible usar objetos. Los objetos nos permitirán resolver los problemas anteriores de una forma segura y elegante.

La resolución de Primitive Obsession es, frecuentemente, Replace Data with Object, que es un refactor en el que encapsulamos el dato, o conjunto de datos, primitivo en un objeto simple.

 1 class Email
 2 {
 3     private string $email;
 4 
 5     public function __construct(string $email)
 6     {
 7         $this->email = $email;
 8     }
 9 
10     public function __toString(): string
11     {
12         return $this->email;
13     }
14 }

Al principio vamos a tener muchas situaciones en las que querremos tener el equivalente primitivo del objeto, por lo que necesitaremos algún tipo de getter. Sin embargo, muchos lenguajes ofrecen la posibilidad de hacer un type casting de un objeto a un primitivo, lo que nos permitirá usar el objeto en cualquier lugar donde se necesite el primitivo. Así, por ejemplo, en PHP se puede contar con el método mágico __toString() el cual es invocado automáticamente cuando hacemos el casting o bien en contextos en el que el dato esperado es un string.

1 $email = new Email("fran.iglesias@example.com");
2 
3 echo (string)$email;

En cualquier caso, una buena recomendación que podemos seguir es asegurar que estos métodos proporcionen una representación primitiva fácilmente parseable. Es decir, que teniendo el string que devuelve __toString() podamos reconstruir el objeto original.

Ya tenemos el objeto, ¿qué ventajas obtenemos?

Así, por ejemplo, podemos hacer que un objeto se cree siempre con valores adecuados, impidiendo que se pueda instanciar si no se cumplen las condiciones requeridas. Veamos aquí el ejemplo con el email:

 1 class Email
 2 {
 3     private string $email;
 4 
 5     public function __construct(string $email)
 6     {
 7         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
 8             throw new InvalidArgumentException('Invalid email address');
 9         }
10         $this->email = $email;
11     }
12 
13     public function __toString(): string
14     {
15         return $this->email;
16     }
17 }

En consecuencia, si tenemos un objeto de tipo Email siempre tendremos la seguridad de que es válido, puesto que no se puede instanciar con valores que no pasen la validación. Además, si necesitamos el email como string, podemos hacer type casting y obtenerlo. Entre otras ventajas, conseguimos reducir la cantidad de código repetitivo que necesitamos para validar los emails. Y en caso de que la validación cambie, solo tendremos que aplicar el cambio en un único lugar, garantizando la coherencia entre partes distintas de la aplicación.

Algunos autores, como Yegor Bugayenko, sostienen que este tipo de constructores con validación no son correctos. La razón es que el constructor debería ser lo más simple posible, y la validación debería hacerse en un método aparte. Entre otras razones, porque hay situaciones en las que podemos confiar en la validez de los parámetros de entrada. Su propuesta es utilizar constructores secundarios que, en el caso de PHP y otros lenguajes, se implementan como métodos de clase estáticos.

 1 class Email
 2 {
 3     private string $email;
 4 
 5     private function __construct(string $email)
 6     {
 7         $this->email = $email;
 8     }
 9 
10     public static function fromString(string $email): self
11     {
12         return new self($email);
13     }
14 
15     public static function valid(string $email): self
16     {
17         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
18             throw new InvalidArgumentException('Invalid email address');
19         }
20         return new self($email);
21     }
22 
23     public function __toString(): string
24     {
25         return $this->email;
26     }
27 }

Resolviendo Data Clump

La solución a Data Clump, como hemos señalado arriba, es Replace Data with Object, encapsulando los datos en un objeto que represente el concepto que estamos modelando. Hay algunos contextos específicos en el que este smell puede resolverse con Introduce Parameter Object, que es un refactor que consiste en agrupar los parámetros que pasamos a una función en objetos ad hoc. Vamos a aplicarlo al problema del nombre de una persona.

1 $firstName = "Pepa";
2 $lastName = "Pérez García";

Podemos crear un objeto SimpleName que encapsule el nombre y el apellido:

 1 class SimpleName
 2 {
 3     private string $name;
 4     private string $lastName;
 5 
 6     public function __construct(string $name, string $lastName)
 7     {
 8         $this->name = $name;
 9         $this->lastName = $lastName;
10     }
11 
12     public function __toString(): string
13     {
14         return $this->name . ' ' . $this->lastName;
15     }
16 }

Ahora podemos modificar la clase Customer para que use este objeto. Este es el objeto Customer que presentamos antes:

 1 class Customer
 2 {
 3     private string $id;
 4     private string $name;
 5     private string $lastName;
 6     
 7     public function __construct(string $id, string $name, string $lastN\
 8 ame) {
 9         $this->id = $id;
10         $this->name = $name;
11         $this->lastName = $lastName;
12     }
13 }

Y este es el objeto Customer que usa SimpleName:

 1 class Customer
 2 {
 3     private string $id;
 4     private SimpleName $personName;
 5     
 6     public function __construct(string $id, SimpleName $personName) {
 7         $this->id = $id;
 8         $this->personName = $personName;
 9     }
10 }

Ahora bien, hay muchos contextos en los que probablemente la información necesaria para inicializar Customer venga en forma de datos primitivos. ¿Deberíamos usar estos directamente o instanciar un SimpleName primero? Por supuesto, la respuesta es que depende. Una solución es introducir varios Factory Method que permitan la creación de un objeto Customer de distintas formas, adecuadas para diferentes situaciones. En este ejemplo, exponemos dos: uno que recibe un nombre y un apellido y otro que recibe un objeto SimpleName.

 1 class Customer
 2 {
 3     private string $id;
 4     private SimpleName $personName;
 5     
 6     private function __construct(string $id, SimpleName $personName) {
 7         $this->id = $id;
 8         $this->personName = $personName;
 9     }
10     
11     public static function fromName(string $id, string $name, string $l\
12 astName): self
13     {
14         $customer = new self($id, new SimpleName($name, $lastName));
15         
16         return $customer;
17     }
18 
19     public static function fromSimpleName(string $id, SimpleName $perso\
20 nName): self
21     {
22         $customer = new self($id, '');
23         $customer->personName = $personName;
24         
25         return $customer;
26     }
27 }

El constructor nativo, primario o canónico, instancia Customer con datos ya encapsulados, que sería la forma canónica, y lo ponemos privado para que no se pueda usar directamente. En su lugar, usamos los métodos de clase fromName y fromSimpleName para crear instancias de Customer, según sea nuestro contexto.

DTOs

Los DTO son objetos muy simples que no tienen comportamiento. Algunos lenguajes utilizan el tipo nativo Struct para representar este tipo de objetos. En otros, podemos usar clases con propiedades públicas, a ser posible de solo lectura, ya que deben ser inmutables. Estas propiedades serán de tipos primitivos del lenguaje, de este modo, la serialización y deserialización de los objetos será más sencilla.

1 class CustomerDTO
2 {
3     public readonly string $id;
4     public readonly string $name;
5     public readonly string $lastName;
6     public readonly string $email;
7 }

Los DTO se utilizan para transferir datos entre subsistemas de una aplicación. Ni siquiera tendrían que ser una representación exacta de un concepto del dominio, sino que simplemente agrupan datos para moverlos de un sitio a otro. Por ejemplo, un DTO que represente los datos de un formulario que se envía a un servidor, o bien un DTO que represente los datos que se devuelven en una respuesta de una API.

Podemos usar DTO para modelar Comandos, Queries y Eventos, ya que son mensajes que emite un subsistema o capa para que otro subsistema, o capa, los interprete y haga algo en respuesta. En general, los DTO son útiles para desacoplar subsistemas y capas, y permiten que los datos se muevan de un sitio a otro sin que los subsistemas tengan que conocerse entre sí.

1 class CreateCustomerCommand
2 {
3     public readonly string $id;
4     public readonly string $name;
5     public readonly string $lastName;
6     public readonly string $email;
7 }

Un uso de los DTO sería resolver el problema de los Data Clumps que mencionábamos antes. Si tenemos un conjunto de datos que se mueve siempre junto, podemos encapsularlos en un DTO y moverlos juntos. Por ejemplo, en el caso de una función que tenga muchos parámetros, podríamos reemplazarlos por un DTO.

1 function createCustomer(string $id, string $name, string $lastName, str\
2 ing $email): void
3 {
4     $customer = new Customer($id, $name, $lastName, $email);
5     // ...
6 }

Algo parecido a esto:

1 function createCustomer(CreateCustomerCommand $command): void
2 {
3     $customer = new Customer($command->id, $command->name, $command->la\
4 stName, $command->email);
5     // ...
6 }

Este refactor se llama Introduce Parameter Object, y es una forma de agrupar los parámetros que pasamos a una función en un objeto que agrupa los datos. Una de las ventajas es que cada dato está identificado por el nombre de la propiedad, lo que hace que sea más fácil de entender qué datos se están pasando a la función y no tener preocuparnos de hacerlo en el orden correcto.

1 $pepaCustomer = new CreateCustomerCommand();
2 
3 $pepaCustomer->id = "123";
4 $pepaCustomer->name = "Pepa";
5 $pepaCustomer->lastName = "Pérez García";
6 $pepaCustomer->email = "pepa@example.com";
7 
8 createCustomer($pepaCustomer);

Tipos

Cuando el sistema de tipos del lenguaje no nos proporciona suficientes garantías, o bien cuando consideramos ciertas restricciones que debemos aplicar frecuentemente, podemos introducir objetos que, sin llegar a representar conceptos de un dominio, nos permiten encapsular ciertas reglas genéricas.

Por ejemplo, imaginemos que queremos representar un número entero que no puede ser negativo. Podríamos hacer algo así:

 1 class NonNegative
 2 {
 3     private int $value;
 4 
 5     public function __construct(int $value)
 6     {
 7         if ($value < 0) {
 8             throw new InvalidArgumentException('Value must be non-negat\
 9 ive');
10         }
11         $this->value = $value;
12     }
13 
14     public function __toString(): string
15     {
16         return (string) $this->value;
17     }
18 }

De este modo, si necesitamos un número no negativo, simplemente creamos un objeto NonNegative y ya tenemos la garantía de que el valor será correcto. Algo similar si lo que necesitamos es strings no vacíos, que sería un requisito habitual para muchos campos de texto:

 1 class NonEmptyString
 2 {
 3     private string $value;
 4 
 5     public function __construct(string $value)
 6     {
 7         if ('' === $value) {
 8             throw new InvalidArgumentException('Value must be non-empty\
 9 ');
10         }
11         $this->value = $value;
12     }
13 
14     public function __toString(): string
15     {
16         return $this->value;
17     }
18 }

Ahora, podríamos definir un objeto de dominio que represente un Customer de la siguiente manera, de tal modo que garantizamos que los datos que contiene son válidos:

 1 class Customer
 2 {
 3     private string $id;
 4     private NonEmptyString $personName;
 5     private NonNegative $age;
 6 
 7     public function __construct(string $id, string $personName, int $ag\
 8 e)
 9     {
10         $this->id = $id;
11         $this->personName = new NonEmptyString($personName);
12         $this->age = new NonNegative($age);
13     }
14 }

Un efecto secundario beneficioso es que leyendo la definición de Customer podemos visualizar también las reglas de validación estructural que se le aplican.

Una observación muy importante es que estos objetos no deben usarse como clases base para derivar Value Objects. No tienen un significado en el dominio, sino que son simplemente objetos que encapsulan ciertas reglas. Los usaremos siempre por composición, como si fuesen tipos nativos.

Enumerables

Es bastante frecuente encontrarnos con ciertos conceptos que se pueden representar con un número finito, reducido y fijo, de valores posibles. Por ejemplo, el estado de un pedido, que puede ser PENDING, SHIPPED, DELIVERED, etc. O bien, el tipo de un producto, que puede ser PHYSICAL, DIGITAL, SERVICE, etc. Y también es el caso de las opciones de un menú, categorías de clasificación, y un largo etcétera.

Cuando no es previsible que estos valores cambien podemos representarlos con un tipo Enumerable. En pocas palabras, un tipo Enumerable es aquel que tiene un número finito y fijo de valores posibles. Por tanto, para instanciar un objeto pasamos el valor y lo validamos contra los valores posibles.

 1 class OrderStatus
 2 {
 3     private const PENDING = 'PENDING';
 4     private const SHIPPED = 'SHIPPED';
 5     private const DELIVERED = 'DELIVERED';
 6 
 7     private string $status;
 8 
 9     public function __construct(string $status)
10     {
11         if (!in_array($status, [self::PENDING, self::SHIPPED, self::DEL\
12 IVERED])) {
13             throw new InvalidArgumentException('Invalid status');
14         }
15         $this->status = $status;
16     }
17 
18     public function __toString(): string
19     {
20         return $this->status;
21     }
22 }

Esto nos permite tener la seguridad de que el estado de un pedido siempre será uno de los valores posibles. Además, si necesitamos añadir un nuevo estado, solo tendremos que modificar la clase OrderStatus.

Ahora bien, esto se refiere a la etiqueta o valor que representa el estado, sobre todo en lo que se refiere a contextos en los que vamos a serializar o deserializar estos valores.

En muchos casos, necesitaremos también un comportamiento asociado, como sería el caso de un OrderStatus que nos permita saber si un pedido está pendiente, enviado o entregado, para determinar si podemos hacer progresar ese pedido a una nueva fase. Lo habitual es consultar de qué estado se trata para decidir qué hacer a continuación. Entonces, lo que necesitamos es un objeto que encapsule el estado y el comportamiento asociado, lo que se conoce como patrón State, del cual hablaremos extensamente en el capítulo correspondiente.

La línea que separa el uso de un tipo Enumerable y un objeto State es bastante difusa, y en muchos casos se solapan. En general, si necesitamos un objeto que represente un estado y tenga comportamiento asociado, lo que necesitamos es un patrón State, representando los distintos estados no ya como valores, sino como objetos. Si solo necesitamos representar un valor que puede ser uno de varios posibles, usaremos un tipo Enumerable.

Por supuesto, en el contexto del patrón State, podemos representar esos posibles valores en forma de Enumerable, al menos de cara a serializar o deserializar, a fin de obtener valores consistentes o reconstruir el objeto original.

Objetos

Si bien los tipos no dejan de ser objetos, aquí pondremos el acento en aquellos que representan conceptos significativos en el dominio. Nos vienen a la cabeza, un Customer o un Product, que son habituales en muchos negocios, pero también podemos representar mediante objetos un File, una Configuration, un Mapper y, en general, cualquier concepto dentro de un programa que se pueda tratar como una unidad capaz de exponer comportamiento.

He aquí un ejemplo simple: un objeto File que abstrae la capacidad de escribir y leer el contenido de archivos en el sistema de archivos local. Este objeto encapsula la lógica de lectura y escritura, y nos permite trabajar con archivos de una forma más sencilla, segura y consistente.

 1 class File
 2 {
 3     private string $filePath;
 4 
 5     public function __construct(string $filePath)
 6     {
 7         $this->filePath = $filePath;
 8     }
 9 
10     public function read(): string
11     {
12         if (!file_exists($this->filePath)) {
13             throw new \RuntimeException("File not found: {$this->filePa\
14 th}");
15         }
16 
17         $contents = file_get_contents($this->filePath);
18         if ($contents === false) {
19             throw new \RuntimeException("Failed to read file: {$this->f\
20 ilePath}");
21         }
22 
23         return $contents;
24     }
25 
26     public function write(string $contents): void
27     {
28         $result = file_put_contents($this->filePath, $contents);
29         if ($result === false) {
30             throw new \RuntimeException("Failed to write to file: {$thi\
31 s->filePath}");
32         }
33     }
34 }

Value Objects

Los value objects son un tipo de objetos que representan algún concepto importante en el dominio de negocio de la aplicación. En resumen, los value objects:

  • Representan conceptos importantes o interesantes del dominio, entendido como el dominio de conocimiento que toca el código que estamos implementando o estudiando.
  • Siempre son creados consistentes, de modo que si obtienes una instancia puedes tener la seguridad de que es válida. De otro modo, no se crean y se lanza una excepción.
  • Los objetos nos interesan por su valor, no por su identidad, por lo que tienen que tener alguna forma de chequear su igualdad.
  • Son inmutables: su valor no puede cambiar durante su ciclo de vida. En caso de que tengan métodos mutators, estos devolverán una nueva instancia de la clase con el valor modificado.
  • Encapsulan comportamientos. Los buenos value objects atraen y encapsulan comportamientos que pueden ser utilizados por el resto del código.

Los value objects pueden ser genéricos y reutilizables, como Money, o muy específicos de un dominio.

Una aclaración que me gustaría hacer es value object es uno de los bloques de construcción en Domain Driven Design, pero el patrón de encapsular valores primitivos en objetos lo podemos, y debemos, aplicar en cualquier tipo de diseño orientado a objetos.

Refactorizar a value objects

Refactorizar a value objects puede ser una tarea de bastante calado, ya que implica crear nuevas clases y utilizarlas en diversos puntos del código. Ahora bien, este proceso puede hacerse de forma bastante gradual. Ten en cuenta que:

  • Los value objects no tienen dependencias, para crearlos solo necesitas primitivos o bien otros value objects.
  • Los value objects se pueden instanciar allí donde los necesites, son newables.
  • Normalmente, tendrás métodos para convertir los value objects a escalares, de modo que puedas utilizar sus valores con código que no puedes modificar.

Los value objects aportan varias ventajas:

  • Al encapsular su validación tendrás objetos con valores adecuados que puedes usar libremente sin necesidad de validar constantemente.
  • Aportarán significado a tu código, siempre sabrás cuando una variable es un precio, un email, una edad, lo que necesites.
  • Te permiten abstraerte de cuestiones como formato, precisión, etc.

Un ejercicio para aprender a usar objetos

Veamos un objeto típico de cualquier negocio: Customer que da lugar a varios ejemplos clásicos de value object. Un cliente siempre suele tener un nombre, que acostumbra a ser una combinación de nombre de pila y uno o más apellidos. También tiene una dirección, que es una combinación de unos cuantos datos.

El siguiente ejercicio que vamos a hacer se inspira en una regla de Object Calisthenics, que nos pide que una clase no tenga más de dos propiedades. Hacer este ejercicio te ayudará a identificar conceptos compuestos en tus objetos. En este caso, vamos a ver cómo podemos aplicar esta regla a un objeto Customer que ahora mismo tiene muchas propiedades:

 1 class Customer
 2 {
 3     private $id;
 4     private $name;
 5     private $firstSurname;
 6     private $lastSurname;
 7     private $street;
 8     private $streetNumber;
 9     private $floor;
10     private $postalCode;
11     private $city;
12 }

El constructor de nuestro Customer podría ser muy complicado, y eso que no hemos incluido todos los campos:

 1 class Customer
 2 {
 3     private $id;
 4     private $name;
 5     private $firstSurname;
 6     private $lastSurname;
 7     private $street;
 8     private $streetNumber;
 9     private $floor;
10     private $postalCode;
11     private $city;
12 
13     public function __construct(
14         string $id,
15         string $name,
16         string $firstSurname,
17         ?string $lastSurname,
18         string $street,
19         string $streetNumber,
20         string $floor,
21         string $postalCode,
22         string $city
23     )
24     {
25         $this->id = $id;
26         $this->name = $name;
27         $this->firstSurname = $firstSurname;
28         $this->lastSurname = $lastSurname;
29         $this->street = $street;
30         $this->streetNumber = $streetNumber;
31         $this->floor = $floor;
32         $this->postalCode = $postalCode;
33         $this->city = $city;
34     }
35 
36     public function fullName(): string 
37     {
38         $fullName = $this->name . ' ' . $this->firstSurname;
39         
40         if ($this->lastSurname) {
41             $fullName .= ' ' . $this->lastSurname;
42         }
43         
44         return $fullName;
45     }
46 
47     public function address(): string 
48     {
49         $address = $this->street . ', ' . $this->streetNumber;
50         
51         if ($this->floor) {
52             $address .= ' '. $this->floor;
53         }
54         
55         $address .= $this->postalCode. '-'.$this->city;
56         
57         return $address;
58     }
59 }

Solemos decir que las cosas que cambian juntas deben ir juntas, pero eso también implica que las cosas que no cambian juntas deberían estar separadas. En el constructor van todos los detalles mezclados y se hace muy difícil de manejar. De hecho, como prácticamente todos los campos son del mismo tipo, es fácil confundirlos. Un error en el orden de los parámetros puede ser muy difícil de detectar.

Una forma de abordar esto es introducir un patrón Builder:

 1 class CustomerBuilder
 2 {
 3     private $name;
 4     private $firstSurname;
 5     private $lastSurname;
 6     private $street;
 7     private $streetNumber;
 8     private $floor;
 9     private $postalCode;
10     private $city;
11 
12     public function withName(string $name, string $firstSurname, ?strin\
13 g $lastSurname) : self
14     {
15         $this->name = $name;
16         $this->firstSurname = $firstSurname;
17         $this->lastSurname = $lastSurname;
18         
19         return $this;
20     }
21 
22     public function withAddress(string $street, string $streetNumber, s\
23 tring $floor, string $postalCode, string $city): self
24     {
25         $this->street = $street;
26         $this->streetNumber = $streetNumber;
27         $this->floor = $floor;
28         $this->postalCode = $postalCode;
29         $this->city = $city;
30         
31         return $this;
32     }
33 
34     public function build() : Customer
35     {
36         return new Customer(
37             $this->id,
38             $this->name,
39             $this->firstSurname,
40             $this->lastSurname,
41             $this->street,
42             $this->streetNumber,
43             $this->floor,
44             $this->postalCode,
45             $this->city
46         );
47     }
48 }

La ventaja del patrón Builder es que nos permite ocultar la complejidad del constructor canónico, introduciendo una interfaz de construcción más significativa. Observa el siguiente código. El resultado es el mismo, pero la forma de construir el objeto es mucho más clara, y eso que la dirección se las trae, con ni más ni menos que cinco campos:

1 $customerBuilder = new CustomerBuilder();
2 
3 $customer = $customerBuilder
4     ->withName('Fran', 'Iglesias', 'Gómez')
5     ->withAddress('Piruleta St', '123', '4', '08030', 'Barcelona')
6     ->build();

Pero, por otro lado, gracias a usar el builder podemos ver que existen, al menos, dos conceptos: el nombre del cliente y su dirección. De hecho, en la dirección tendríamos también dos conceptos: la localidad y las señas dentro de esa localidad. En realidad tenemos casos de Data Clump que podríamos resolver con objetos.

Vamos por partes:

Introduciendo objetos

Parece que no, pero manejamos mucha lógica en algo tan simple como un nombre. Veamos por ejemplo:

  • En España usamos nombres con dos apellidos, pero en muchos otros países se suele usar un nombre con un único apellido.
  • A veces necesitamos usar partes del nombre por separado, como sería el nombre de pila (“Estimada Susana”, “Sr. Pérez”). Otras veces queremos combinarlo de diferentes formas, como podría ser poner el apellido primero, lo que es útil para listados.
  • Y, ¿qué pasa si queremos introducir nueva información relacionada con el nombre? Por ejemplo, el tratamiento (Sr./Sra., Estimado/Estimada, etc.).

El nombre del cliente se puede convertir fácilmente a un objeto, lo que retirará cualquier lógica de la “gestión” del nombre de la clase Customer, contribuyendo al Single Responsibility Principle y proporcionándonos un comportamiento reutilizable.

Así que podemos crear un objeto sencillo:

 1 class PersonName
 2 {
 3     private string $name;
 4     private string $firstSurname;
 5     private string $lastSurname;
 6 
 7     public function __construct(string $name, string $firstSurname, str\
 8 ing $lastSurname)
 9     {
10         $this->name = $name;
11         $this->firstSurname = $firstSurname;
12         $this->lastSurname = $lastSurname;
13     }
14 }

Ahora bien, una persona tiene que tener un nombre, no tiene sentido tener objetos PersonName que estén vacíos. Para nuestro ejemplo, las reglas son que Name y FirstSurname son obligatorios y no pueden ser un string vacío. LastSurname es opcional.

Una forma bastante bonita de hacer esto es hacer uso de tipos como NonEmptyString y String.

 1 class NonEmptyString
 2 {
 3     private string $value;
 4 
 5     public function __construct(string $value)
 6     {
 7         if ('' === $value) {
 8             throw new InvalidArgumentException('Value must be non-empty\
 9 ');
10         }
11         $this->value = $value;
12     }
13 
14     public function __toString(): string
15     {
16         return $this->value;
17     }
18 }
 1 class String
 2 {
 3     private string $value;
 4 
 5     public function __construct(string $value)
 6     {
 7         $this->value = $value;
 8     }
 9 
10     public function __toString(): string
11     {
12         return $this->value;
13     }
14 }

Así que lo podemos representar de la siguiente forma:

 1 class PersonName
 2 {
 3     private NonEmptyString $name;
 4     private NonEmptyString $firstSurname;
 5     private String $lastSurname;
 6 
 7     public function __construct(string $name, string $firstSurname, ?st\
 8 ring $lastSurname = null)
 9     {
10         $this->name = new NonEmptyString($name);
11         $this->firstSurname = new NonEmptyString($firstSurname);
12         $this->lastSurname = new String($lastSurname);
13     }
14 }

Por si te lo estabas preguntando, realmente no puedo considerar seriamente PersonName como un value object. No tiene comportamiento de negocio relevante. Quiero decir, es importante gestionar bien el nombre de las personas, pero seguramente no forma parte de la lógica de negocio de tu aplicación. En cualquier caso, es un buen ejemplo para aprender a trabajar con objetos.

Más adelante volveremos sobre este objeto. Ahora vamos a definir varios value objects. De momento, solo me voy a concentrar en los constructores, sin añadir ningún comportamiento, ni siquiera el método equals ya que quiere centrarme en cómo movernos de usar escalares a estos objetos.

Objetos compuestos de otros objetos

Para tratas las direcciones postales haremos algo parecido y crearemos una clase Address para representar las direcciones de los clientes.

Sin embargo, hemos dicho que podríamos introducir un objeto para el concepto de localidad, que incluiría el código postal y la ciudad, pues son datos que van estrechamente relacionados. Obviamente, esto dependerá de nuestro dominio. En algunos casos no nos hará falta esa granularidad porque simplemente queremos disponer de una dirección postal de nuestros clientes para enviar comunicaciones. Pero en otros casos puede ocurrir que nuestro negocio tenga aspectos que dependan de ese concepto, como un servicio cuya tarifa sea función de la ubicación.

 1 class Locality
 2 {
 3     private string $postalCode;
 4     private string $locality;
 5 
 6     public function __construct(string $postalCode, string $locality)
 7     {
 8         $this->isValidPostalCode($postalCode);
 9         $this->isValidLocality($locality);
10         
11         $this->postalCode = $postalCode;
12         $this->locality = $locality;
13     }
14 
15     private function isValidPostalCode(string $postalCode) : void
16     {
17         if (\strlen($postalCode) !== 5 || (int) substr($postalCode, 0, \
18 2) > 52) {
19             throw new InvalidArgumentException('Invalid Postal Code');
20         }
21     }
22     
23     private function isValidLocality(string $locality) : void
24     {
25         if ($locality === '') {
26             throw new InvalidArgumentException('Locality should have a \
27 value');
28         }
29     }

Como se puede ver, tendría sentido introducir un objeto PostalCode, porque tiene unas reglas específicas de validación. En este caso, el código postal debe tener 5 caracteres y los dos primeros no pueden ser mayores de 52, que es el número de provincias en España:

 1 class PostalCode
 2 {
 3     private string $postalCode;
 4 
 5     public function __construct(string $postalCode)
 6     {
 7         $this->isValidPostalCode($postalCode);
 8 
 9         $this->postalCode = $postalCode;
10     }
11 
12     private function isValidPostalCode(string $postalCode) : void
13     {
14         if (\strlen($postalCode) !== 5 || (int) substr($postalCode, 0, \
15 2) > 52) {
16             throw new InvalidArgumentException('Invalid Postal Code');
17         }
18     }
19 }

Aparte de eso, debería haber un nombre de localidad, por lo queLocality podría quedar así:

 1 class Locality
 2 {
 3     private PostalCode $postalCode;
 4     private NonEmptyString $locality;
 5 
 6     public function __construct(string $postalCode, string $locality)
 7     {
 8         $this->postalCode = new PostalCode($postalCode);
 9         $this->locality = new NonEmptyString($locality);
10     }
11 }

En fin. Volviendo a nuestro problema original de crear un objeto Address podríamos adoptar este enfoque:

 1 class Address
 2 {
 3     private string $street;
 4     private string $streetNumber;
 5     private string $floor;
 6     private Locality $locality;
 7 
 8     public function __construct(string $street, string $streetNumber, ?\
 9 string $floor, Locality $locality)
10     {
11         if ('' === $street || '' === $streetNumber) {
12             throw new InvalidArgumentException('Address should include \
13 street and number');   
14         } 
15         $this->street = $street;
16         $this->streetNumber = $streetNumber;
17         $this->floor = $floor;
18         $this->locality = $locality;
19     }
20 }

Pero como antes hemos definido tipos que nos proporcionan ciertas garantías:

 1 class Address
 2 {
 3     private NonEmptyString $street;
 4     private NonEmptyString $streetNumber;
 5     private String $floor;
 6     private Locality $locality;
 7 
 8     public function __construct(string $street, string $streetNumber, ?\
 9 string $floor, Locality $locality)
10     {
11         $this->street = new NonEmptyString($street);
12         $this->streetNumber = new NonEmptyString($streetNumber);
13         $this->floor = new String($floor);
14         $this->locality = $locality;
15     }
16 }

Siempre que un objeto requiere muchos parámetros en su construcción puede ser interesante plantearse si tenemos buenas razones para organizarlos en un objeto, aplicando el principio de co-variación: si cambian juntos, deberían ir juntos. En este caso, $street, $streetNumber y $floor pueden ir juntos, en forma de StreetAddress porque entre los tres componen un concepto útil.

 1 class StreetAddress
 2 {
 3     private NonEmptyString $street;
 4     private NonEmptyString $streetNumber;
 5     private String $floor;
 6 
 7     public function __construct(string $street, string $streetNumber, ?\
 8 string $floor)
 9     {
10         $this->street = new NonEmptyString($street);
11         $this->streetNumber = new NonEmptyString($streetNumber);
12         $this->floor = new String($floor);
13     }
14 }

De este modo, Address se hace más simple y ni siquiera tiene que ocuparse de validar nada:

 1 class Address
 2 {
 3     private StreetAddress $streetAddress;
 4     private Locality $locality;
 5 
 6     public function __construct(StreetAddress $streetAddress, Locality \
 7 $locality)
 8     {
 9         $this->streetAddress = $streetAddress;
10         $this->locality = $locality;
11     }
12 }

En resumidas cuentas, a medida que reflexionamos sobre los conceptos del dominio podemos percibir la necesidad de trasladar esa reflexión al código de una forma más articulada y precisa. Pero como hemos señalado antes todo depende de las necesidades de nuestro dominio. Lo cierto es que, como veremos a lo largo del artículo, cuanto más articulado tengamos el dominio, vamos a tener más capacidad de maniobra y muchísima más coherencia.

Usando los objetos

Volvamos a Customer. De momento, el hecho de introducir una serie de objetos no afecta para nada al código que tengamos, por lo que podríamos estar creando cada uno de ellos, mezclando en el proyecto y desplegando sin afectar de ningún modo a la funcionalidad existente. Simplemente, hemos añadido clases a nuestra base de código y ahí están: esperando a ser utilizadas.

En este caso, tener a CustomerBuilder nos viene muy bien, pues encapsula la compleja construcción de Customer, aislándola del resto del código. Podremos refactorizar Customer sin afectar a nadie. Empezaremos por el nombre:

 1 class Customer
 2 {
 3     private string $id;
 4     private PersonName $personName;
 5     private string $street;
 6     private string $streetNumber;
 7     private string $floor;
 8     private string $postalCode;
 9     private string $city;
10 
11     public function __construct(
12         string $id,
13         PersonName $personName,
14         string $street,
15         string $streetNumber,
16         string $floor,
17         string $postalCode,
18         string $city
19     ) {
20         $this->id = $id;
21         $this->personName = $personName;
22         $this->street = $street;
23         $this->streetNumber = $streetNumber;
24         $this->floor = $floor;
25         $this->postalCode = $postalCode;
26         $this->city = $city;
27     }
28 
29     public function fullName(): string
30     {
31         return $this->personName->fullName();
32     }
33 
34     public function address(): string
35     {
36         $address = $this->street . ', ' . $this->streetNumber;
37 
38         if ($this->floor) {
39             $address .= ' '. $this->floor;
40         }
41 
42         $address .= $this->postalCode. '-'.$this->city;
43 
44         return $address;
45     }
46 }

El constructor ya es un poco más simple. Además, el método fullName puede delegarse al disponible en el objeto PersonName, que se puede ocupar cómodamente de cualquier variante o formato particular que necesitemos a lo largo de la aplicación.

 1 class PersonName
 2 {
 3     private NonEmptyString $name;
 4     private NonEmptyString $firstSurname;
 5     private String $lastSurname;
 6 
 7     public function __construct(string $name, string $firstSurname, ?st\
 8 ring $lastSurname = null)
 9     {
10         $this->name = new NonEmptyString($name);
11         $this->firstSurname = new NonEmptyString($firstSurname);
12         $this->lastSurname = new String($lastSurname);
13     }
14 
15     public function fullName(): string
16     {
17         $fullName = (string)$this->name . ' ' . (string)$this->firstSur\
18 name;
19         
20         if (!$this->lastSurname->empty() {
21             $fullName .= ' ' . $this->lastSurname;
22         }
23         
24         return $fullName;
25     }
26 }

Como podemos ver, los objetos atraen comportamiento. Si necesitásemos el nombre en un formato apto para listas podríamos hacer lo siguiente:

 1 class PersonName
 2 {
 3     private NonEmptyString $name;
 4     private NonEmptyString $firstSurname;
 5     private String $lastSurname;
 6 
 7     public function __construct(string $name, string $firstSurname, ?st\
 8 ring $lastSurname = null)
 9     {
10         $this->name = new NonEmptyString($name);
11         $this->firstSurname = new NonEmptyString($firstSurname);
12         $this->lastSurname = new String($lastSurname);
13     }
14 
15     public function fullName(): string
16     {
17         return (string)$this->name .' ' . $this->surname();
18     }
19 
20     public function listName(): string
21     {
22         return $this->surname() . ', ' . (string)$this->name;
23     }
24 
25     public function surname(): string
26     {
27         $surname = (string)$this->firstSurname;
28 
29         if (!$this->lastSurname->empty()) {
30             $surname .= ' ' . $this->lastSurname;
31         }
32 
33         return $surname;
34     }
35 }

Como tenemos un Builder que encapsula la construcción de Customer, lo que hacemos es modificar esa construcción de acuerdo al nuevo diseño:

 1 class CustomerBuilder
 2 {
 3     private string $id;
 4     private PersonName $personName;
 5     private string $street;
 6     private string $streetNumber;
 7     private string $floor;
 8     private string $postalCode;
 9     private string $city;
10 
11     public function withName(string $name, string $firstSurname, ?strin\
12 g $lastSurname) : self
13     {
14         $this->personName = new PersonName($name, $firstSurname, $lastS\
15 urname);
16         
17         return $this;
18     }
19 
20     public function withAddress(string $street, string $streetNumber, s\
21 tring $floor, string $postalCode, string $city): self
22     {
23         $this->street = $street;
24         $this->streetNumber = $streetNumber;
25         $this->floor = $floor;
26         $this->postalCode = $postalCode;
27         $this->city = $city;
28         
29         return $this;
30     }
31 
32     public function build() : Customer
33     {
34         return new Customer(
35             $this->id,
36             $this->personName,
37             $this->street,
38             $this->streetNumber,
39             $this->floor,
40             $this->postalCode,
41             $this->city
42         );
43     }
44 }

Fíjate que he dejado el método withName() tal y como estaba. De esta forma, no cambio la interfaz pública de CustomerBuilder, como tampoco cambia la de Customer salvo en el constructor, y el código que lo usa no se enterará del cambio. En otras palabras, el ejemplo anterior funcionará exactamente igual:

1 $customerBuilder = new CustomerBuilder();
2 
3 $customer = $customerBuilder
4     ->withName('Fran', 'Iglesias', 'Gómez')
5     ->withAddress('Piruleta St', '123', '4', '08030', 'Barcelona')
6     ->build();

Por supuesto, haríamos lo mismo con el objeto Address. Por tanto, así quedará Customer:

 1 class Customer
 2 {
 3     private $id;
 4     private PersonName $personName;
 5     private Address $address;
 6 
 7     public function __construct(
 8         string $id,
 9         PersonName $personName,
10         Address $address
11     ) {
12         $this->id = $id;
13         $this->personName = $personName;
14         $this->address = $address;
15     }
16 
17     public function fullName(): string
18     {
19         return $this->personName->fullName();
20     }
21 
22     public function address(): string
23     {
24         return $this->address->full();
25     }
26 }

El método full en Address queda como sigue:

 1 class Address
 2 {
 3     private StreetAddress $streetAddress;
 4     private Locality $locality;
 5 
 6     public function __construct(StreetAddress $streetAddress, Locality \
 7 $locality)
 8     {
 9         $this->streetAddress = $streetAddress;
10         $this->locality = $locality;
11     }
12 
13     public function full(): string
14     {
15         return (string)$this->streetAddress . ' ' . (string)$this->loca\
16 lity;
17     }
18 }

En este caso necesitaremos:

 1 class StreetAddress
 2 {
 3 
 4     private NonEmptyString $street;
 5     private NonEmptyString $streetNumber;
 6     private String $floor;
 7 
 8     public function __construct(string $street, string $streetNumber, ?\
 9 string $floor)
10     {
11         $this->street = new NonEmptyString($street);
12         $this->streetNumber = new NonEmptyString($streetNumber);
13         $this->floor = new String($floor);
14     }
15 
16     public function __toString(): string 
17     {
18         $fullAddress = (string)$this->street . ' ' . (string)$this->str\
19 eetNumber;
20         
21         if (!$this->floor->empty()) {
22             $fullAddress .= ', '. $this->floor;
23         }
24         
25         return $fullAddress;
26     }
27 }

Y también:

 1 class PostalCode
 2 {
 3     private string $postalCode;
 4 
 5     public function __construct(string $postalCode)
 6     {
 7         $this->isValidPostalCode($postalCode);
 8 
 9         $this->postalCode = $postalCode;
10     }
11 
12     private function isValidPostalCode(string $postalCode) : void
13     {
14         if (\strlen($postalCode) !== 5 || (int) substr($postalCode, 0, \
15 2) > 52) {
16             throw new InvalidArgumentException('Invalid Postal Code');
17         }
18     }
19 
20     public function __toString(): string 
21     {
22         return $this->postalCode;
23     }
24 }

Así como:

 1 class Locality
 2 {
 3     private PostalCode $postalCode;
 4     private NonEmptyString $locality;
 5 
 6     public function __construct(string $postalCode, string $locality)
 7     {
 8         $this->postalCode = new PostalCode($postalCode);
 9         $this->locality = new NonEmptyString($locality);
10     }
11 
12     public function __toString(): string 
13     {
14         return (string)$this->postalCode .'-'.(string)$this->locality;
15     }
16 }

Del mismo modo que antes, modificaremos CustomerBuilder para utilizar los nuevos objetos:

 1 class CustomerBuilder
 2 {
 3     private $personName;
 4     private $address;
 5 
 6     public function withName(string $name, string $firstSurname, ?strin\
 7 g $lastSurname) : self
 8     {
 9         $this->personName = new PersonName($name, $firstSurname, $lastS\
10 urname);
11         
12         return $this;
13     }
14 
15     public function withAddress(string $street, string $streetNumber, s\
16 tring $floor, string $postalCode, string $city) : self
17     {
18         $locality = new Locality($postalCode, $city);
19         $streetAddress = new StreetAddress($street, $streetNumber, $flo\
20 or);
21 
22         $this->address = new Address($streetAddress, $locality);
23         
24         return $this;
25     }
26 
27     public function build() : Customer
28     {
29         return new Customer(
30             $this->id,
31             $this->personName,
32             $this->address
33         );
34     }
35 }

Y ya está, hemos hecho este cambio sin tener que tocar en ningún lugar más del código. Obviamente, tener un Builder de Customer nos ha facilitado muchos las cosas. En general, para hacer este tipo de refactorizaciones, es útil tener alternativas al constructor canónico, como el propio Builder.

Beneficios

El beneficio más evidente es que las clases importantes del dominio como Customer, quedan mucho más compactas. Hemos podido reducir ocho propiedades a dos, cumpliendo la regla de Calisthenics. Y, además, son conceptos relevantes dentro de Customer.

Por otro lado, Customer delega todos los detalles a esos objetos. Dicho de otro modo, Customer no tiene que saber cómo se da formato a un nombre o a una dirección. Simplemente, cuando se lo piden entrega el nombre o la dirección formateados. Asimismo, cualquier otro objeto que usase PersonName o Address, lo hará de la misma manera.

Otra cosa interesante es que los cambios que necesitemos en el comportamiento de estas propiedades pueden aplicarse sin tocar el código de la clase, modificando o cambiando los objetos, con lo cual el nuevo comportamiento se extenderá a todas las partes de la aplicación que lo utilicen.

Sin embargo, nuestro dominio tiene ahora muchísima flexibilidad y capacidad de cambio.

Sería bastante fácil, por ejemplo, dar soporte a los múltiples formatos de dirección postal que se usan en todo el mundo, de modo que nuestro negocio está mejor preparado para expandirse internacionalmente, puesto que solo tendríamos que introducir una interfaz y nuevos formatos a medida que los necesitemos, sin tener que cambiar el core del dominio. Puede sonar exagerado, pero estos pequeños detalles pueden ser un dolor de cabeza enorme si seguimos el modelo con el que empezamos. Algo tan pequeño puede ser la diferencia entre un código y un negocio que escale fácilmente o no.

Resumen del capítulo

Encapsular tipos primitivos en objetos es una receta de éxito para lograr código más simple y, a la vez, ganar en consistencia y flexibilidad. El código tiene que preocuparse menos por conocer el detalle de los datos, centrándose en cómo interactúan los objetos y colaboran entre ellos para lograr el propósito de la aplicación.

Acondiciona las condicionales

En el que decidimos cómo hacer que las decisiones que el código toma sean más comprensibles y fáciles de mantener en el futuro, porque al fin y a la postre todo en esta vida es decidir. El caso es saber cuando tomar las decisiones y comprender bien sus condiciones y sus consecuencias.

Notas de la segunda edición

En este capítulo, como en otros, hemos mejorado los ejemplos y corregido algunas explicaciones para que sean más claras. Ahora la mayor parte de ejemplos tienen más sentido y ofrecen más contexto. Asímismo hemos incluido algunas propuestas nuevas.

La complejidad de decidir

Es bastante obvio que si hay algo que añade complejidad a un software es la toma de decisiones y, por tanto, las estructuras condicionales con las que la expresamos.

Estas estructuras pueden introducir dificultades de comprensión debido a varias razones:

  • La complejidad de las expresiones evaluadas, sobre todo cuando se combinan mediante operadores lógicos tres o más condiciones, lo que las hace difíciles de procesar para nosotras.
  • La anidación de estructuras condicionales y la concatenación de condicionales mediante else, introduciendo múltiples flujos de ejecución.
  • El desequilibrio entre las ramas en las que una rama tiene unas pocas líneas frente a la otra que esconde su propia complejidad, lo que puede llevar a pasar por alto la rama corta y dificulta la lectura de la rama larga al introducir un nuevo nivel de indentación.

¿Cuándo refactorizar condicionales?

En general, como regla práctica, hay que refactorizar condicionales cuando su lectura no nos deja claro cuál es su significado. Esto se aplica en las dos partes de la estructura:

  • La expresión condicional: qué define lo que tiene que pasar para que el flujo se dirija por una o por otra rama.
  • Las ramas: las diferentes acciones que se deben ejecutar en caso de cumplirse o no la condición.

También podemos aplicar alguna de las reglas de object calisthenics:

Aplanar niveles de indentación: cuanto menos anidamiento en el código, más fácil de leer es porque indica que no estamos mezclando niveles de abstracción. El objetivo sería tener un solo nivel de indentación en cada método o función.

Eliminar else: en muchos casos, es posible eliminar ramas alternativas, bien directamente, bien encapsulando toda la estructura en un método o función.

Vamos a ver como podemos proceder a refactorizar condicionales de una forma sistemática.

La rama corta primero

Si una estructura condicional nos lleva por una rama muy corta en caso de cumplirse y por una muy larga en el caso contrario, se recomienda que la rama corta sea la primera, para evitar que pase desapercibida. Por ejemplo, este fragmento tan feo:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod === null) {
 4         $logger = Logger::getInstance();
 5         $logger->debug("Medio de pago desconocido");
 6         if ($order->getDestinationCountry() == Country::FRANCE && $orde\
 7 r->id() < 745) {
 8             $paymentMethod = new PaypalPaymentMethod();
 9         } else {
10             $paymentMethod = new DefaultPaymentMethod();
11         }
12     } else {
13         $paymentMethod = $selectedPaymentMethod;
14     }
15     
16     return $paymentMethod;
17 }

Podría reescribirse así y ahora es más fácil ver la rama corta. Esto nos va a encaminar hacia otras refactorizaciones que veremos más adelante:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         $paymentMethod = $selectedPaymentMethod;
 5     } else {
 6         $logger = Logger::getInstance();
 7         $logger->debug("Medio de pago desconocido");
 8         if ($order->getDestinationCountry() == Country::FRANCE && $orde\
 9 r->id() < 745) {
10             $paymentMethod = new PaypalPaymentMethod();
11         } else {
12             $paymentMethod = new DefaultPaymentMethod();
13         }
14     }
15     
16     return $paymentMethod;
17 }

Return early

Si estamos dentro de una función o método y podemos hacer el retorno desde dentro de una rama es preferible hacerlo. Con eso podemos evitar la cláusula else y hacer que el código vuelva al nivel de indentación anterior, lo que facilitará la lectura. Veámoslo aplicado al ejemplo de arriba:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         $paymentMethod = $selectedPaymentMethod;
 5         
 6         return $paymentMethod;
 7     } else {
 8         $logger = Logger::getInstance();
 9         $logger->debug("Medio de pago desconocido");
10         if ($order->getDestinationCountry() == Country::FRANCE && $orde\
11 r->id() < 745) {
12             $paymentMethod = new PaypalPaymentMethod();
13         } else {
14             $paymentMethod = new DefaultPaymentMethod();
15         }
16     }
17     
18     return $paymentMethod;
19 }

No hace falta mantener la variable temporal, pues podemos devolver directamente la respuesta obtenida:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     } else {
 6         $logger = Logger::getInstance();
 7         $logger->debug("Medio de pago desconocido");
 8         if ($order->getDestinationCountry() == Country::FRANCE && $orde\
 9 r->id() < 745) {
10             $paymentMethod = new PaypalPaymentMethod();
11         } else {
12             $paymentMethod = new DefaultPaymentMethod();
13         }
14         
15         return $paymentMethod;
16     }
17 }

Y, por último, podemos eliminar la cláusula else y dejarlo así.

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
10 d() < 745) {
11         $paymentMethod = new PaypalPaymentMethod();
12     } else {
13         $paymentMethod = new DefaultPaymentMethod();
14     }
15     
16     return $paymentMethod;
17 }

Aplicando el mismo principio, reducimos la complejidad aportada por la condicional final, retornando directamente y eliminando tanto else como las variables temporales:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     
10     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
11 d() < 745) {
12         return new PaypalPaymentMethod();
13     }
14     
15     return new DefaultPaymentMethod();
16 }

El contexto habitual de esta técnica es la de tratar casos particulares o que sean obvios en los primeros pasos del algoritmo, volviendo al flujo principal cuanto antes, de modo que solo recibe aquellos casos a los que se aplica realmente. Una variante de esta idea consiste en la introducción de las cláusulas de guarda, que veremos a continuación.

Cláusulas de guarda

En muchas ocasiones, cuando los datos tienen que ser validados antes de operar con ellos, podemos encapsular esas condiciones en forma de cláusulas de guarda. Estas cláusulas de guarda, también se conocen como aserciones, o precondiciones. Si los parámetros recibidos no las cumplen, el método o función falla, generalmente, lanzando excepciones.

1 public function applyDiscount($parameter)
2 {
3     if ($parameter > 100 || $parameter < 0) {
4         throw new OutOfRangeException(sprintf('Parameter should be betw\
5 een 0 and 100 (inc), %s provided.', $parameter));
6     }
7     
8     // further processing
9 }

Extraemos toda la estructura a un método privado:

 1 public function applyDiscount($parameter)
 2 {
 3     $this->checkParameterIsInRange($parameter);
 4         
 5     // further processing
 6 }
 7 
 8 private function checkParameterIsInRange($parameter) 
 9 {
10     if ($parameter > 100 || $parameter < 0) {
11         throw new OutOfRangeException(sprintf('Parameter should be betw\
12 een 0 and 100 (inc), %s provided.', $parameter));
13     }
14 }

La lógica bajo este tipo de cláusulas es que si no salta ninguna excepción, quiere decir que $parameter ha superado todas las validaciones y lo puedes usar con confianza. La ventaja es que las reglas de validación definidas con estas técnicas resultan muy expresivas, ocultando los detalles técnicos en los métodos extraídos.

Una alternativa es usar una librería de aserciones, lo que nos permite hacer lo mismo de una forma aún más limpia y reutilizable. Si la aserción no se cumple, se tirará una excepción:

1 public function applyDiscount($parameter)
2 {
3     Assert::betweenExclusive($parameter, 0, 100);
4         
5     // further processing
6 }

Una limitación de las aserciones que debemos tener en cuenta es que no sirven para control de flujo. Esto es, las aserciones fallan con una excepción, interrumpiendo la ejecución del programa, que así puede comunicar al módulo llamante una circunstancia que impide continuar. Por eso, las aserciones son ideales para validar precondiciones.

En el caso de necesitar una alternativa si el parámetro no cumple los requisitos, utilizaremos condicionales. Por ejemplo, si el parámetro excede los límites queremos que se ajuste al límite que ha superado, en vez de fallar:

 1 $parameter = $this->checkTheParameterIsInRange($parameter);
 2 
 3 // further processing
 4 
 5 private function checkTheParameterIsInRange(int $parameter)
 6 {
 7     if ($parameter > 100) {
 8         return 100;
 9     }
10     
11     if ($parameter < 0) {
12         return 0;
13     }
14     
15     return $parameter;
16 }

Finalmente, ten en cuenta que el tipado ya es una guarda en sí misma, por lo que no necesitas verificar el tipo de un parámetro si ya lo has tipado correctamente.

Preferir condiciones afirmativas

Diversos estudios han mostrado que las frases afirmativas son más fáciles de entender que las negativas, por lo que siempre que sea posible deberíamos intentar convertir la condición en afirmativa ya sea invirtiéndola, ya sea encapsulándola de modo que se exprese de manera afirmativa. Con frecuencia, además, la particular sintaxis de la negación puede hacerlas poco visibles:

1 if (!$selectedPaymentMethod) {
2     return $selectedPaymentMethod;
3 } 

En uno de los ejemplos anteriores habíamos llegado a la siguiente construcción invirtiendo la condicional lo que resultó en una doble negación que puede hacerse difícil de leer:

1 if (null !== $selectedPaymentMethod) {
2     return $selectedPaymentMethod;
3 } 

Nosotros lo que queremos es devolver el método de pago en caso de tener uno seleccionado:

1 if ($selectedPaymentMethod) {
2     return $selectedPaymentMethod;
3 } 

Una forma alternativa, si la condición es compleja o simplemente difícil de entender tal cual es encapsularla en un método:

1 if ($this->userHasSelectedAPaymentMethod($selectedPaymentMethod)) {
2     return $selectedPaymentMethod;
3 } 
4 
5 function userHasSelectedAPaymentMethod($selectedPaymentMethod)
6 {
7     return null !== $selectedPaymentMethod;
8 }

Encapsula expresiones complejas en métodos o funciones

La idea es encapsular expresiones condicionales complejas en funciones o métodos, de modo que su nombre describa el significado de la expresión condicional, manteniendo ocultos los detalles escabrosos de la misma. Esto puede hacerse de forma global o por partes.

Justo en el apartado anterior hemos visto un ejemplo de esto mismo, haciendo explícito el significado de una expresión condicional difícil de leer.

Veamos otro caso en el mismo ejemplo: la extraña condicional que selecciona el método de pago en función del país de destino y el número de pedido. Posiblemente, algún apaño para resolver un problema concreto en los primeros pasos de la empresa.

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     
10     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
11 d() < 745) {
12         return new PaypalPaymentMethod();
13     }
14     
15     return new DefaultPaymentMethod();
16 }

Se podría encapsular la condición en un método que sea un poco más explicativo:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     
10     if ($this->isLegacyOrderWithDestinationFrance($order)) {
11         return new PaypalPaymentMethod();
12     }
13     
14     return new DefaultPaymentMethod();
15 }

Y el método encapsulado sería algo así:

1 private function isLegacyOrderWithDestinationFrance($order)
2 {
3     return $order->getDestinationCountry() == Country::FRANCE && $order\
4 ->id() < 745;
5 }

Encapsula ramas en métodos o funciones

Consiste en encapsular todo el bloque de código de cada rama de ejecución en su propio método, de modo que el nombre nos indique qué hace. Esto nos deja las ramas de la estructura condicional al mismo nivel y expresando lo que hacen de manera explícita y global. En los métodos extraídos podemos seguir aplicando refactors progresivos hasta que ya no sea necesario.

Este fragmento de código, que está bastante limpio, podría clarificarse un poco, encapsulando tanto las condiciones como la rama:

 1 if ($productStatus == OrderStatuses::PROVIDER_PENDING ||
 2     $productStatus == OrderStatuses::PENDING ||
 3     $productStatus == OrderStatuses::WAITING_FOR_PAYMENT
 4 ) {
 5     if ($paymentMethod == new BankTransferPaymentMethod()) {
 6         return 'pendiente de transferencia';
 7     }
 8     if ($paymentMethod == new PaypalPaymentMethod() || $paymentMethod =\
 9 = new CreditCardPaymentMethod()) {
10         return 'pago a crédito';
11     }
12     if ($this->paymentMethods->hasSelectedDebitCard()) {
13         return 'pago a débito';
14     }
15     if (!$this->paymentMethods->requiresAuthorization()) {
16         return 'pago no requiere autorización';
17     }
18 }

Veamos como:

 1 if ($this->productIsInPendingStatus($productStatus)) {
 2     return $this->reportForProductInPendingStatus($paymentMethod);
 3 }
 4 
 5 private function productIsInPendingStatus($productStatus)
 6 {
 7     return ($productStatus == OrderStatuses::PROVIDER_PENDING ||
 8     $productStatus == OrderStatuses::PENDING ||
 9     $productStatus == OrderStatuses::WAITING_FOR_PAYMENT);
10 }
11 
12 private function reportForProductInPendingStatus(paymentMethod)
13 {
14     if ($paymentMethod == new BankTransferPaymentMethod()) {
15         return 'pendiente de transferencia';
16     }
17     if ($paymentMethod == new PaypalPaymentMethod() || $paymentMethod =\
18 = new CreditCardPaymentMethod()) {
19         return 'pago a crédito';
20     }
21     if ($this->paymentMethods->hasSelectedDebitCard()) {
22         return 'pago a débito';
23     }
24     if (!$this->paymentMethods->requiresAuthorization()) {
25         return 'pago no requiere autorización';
26     }
27 }

De ese modo, la complejidad queda oculta en los métodos y el cuerpo principal se entiende fácilmente. Ya es cuestión nuestra si necesitamos seguir el refactor dentro de los métodos privados que acabamos de crear.

Equalize branches

Si hacemos esto en todas las ramas de una condicional o de un switch las dejaremos al mismo nivel, lo que facilita su lectura.

Reemplaza if…elseif sucesivos con switch

En muchos casos, sucesiones de if o if…else quedarán mejor expresados mediante una estructura switch o match. Por ejemplo, siguiendo con el ejemplo anterior, este método que hemos extraído:

 1 private function reportForProductInPendingStatus(paymentMethod)
 2 {
 3     if ($paymentMethod == new BankTransferPaymentMethod()) {
 4         return 'pendiente de transferencia';
 5     }
 6     if ($paymentMethod == new PaypalPaymentMethod() || $paymentMethod =\
 7 = new CreditCardPaymentMethod()) {
 8         return 'pago a crédito';
 9     }
10     if ($this->paymentMethods->hasSelectedDebitCard()) {
11         return 'pago a débito';
12     }
13     if (!$this->paymentMethods->requiresAuthorization()) {
14         return 'pago no requiere autorización';
15     }
16 }

Podría convertirse en algo así:

 1 private function reportForProductInPendingStatus(paymentMethod)
 2 {
 3     switch $paymentMethod {
 4         case new BankTransferPaymentMethod():
 5             return 'pendiente de transferencia';
 6         case new PaypalPaymentMethod():
 7         case new CreditCardPaymentMethod():
 8             return 'pago a crédito';
 9     }
10     
11     if ($this->paymentMethods->hasSelectedDebitCard()) {
12         return 'pago a débito';
13     }
14     if (!$this->paymentMethods->requiresAuthorization()) {
15         return 'pago no requiere autorización';
16     }
17 }

Sustituir if por el operador ternario

A veces, aunque suelen ser pocas, un operador ternario puede ser más legible que una condicional:

 1 function selectElement(Criteria $criteria, Desirability $desirability)
 2 {
 3     $found = false;
 4     
 5     $elements = $this->getElements($criteria);
 6     
 7     foreach($elements as $element) {
 8         if (!$found && $this->isDesired($element, $desirability)) {
 9             $result = $element;
10             $found = true;
11         }
12     }
13     if (!$found) {
14         $result = null;
15     }
16     
17     return $result;
18 }

Realmente las últimas líneas pueden expresarse en una sola y queda más claro:

 1 function selectElement(Criteria $criteria, Desirability $desirability)
 2 {
 3     $found = false;
 4     
 5     $elements = $this->getElements($criteria);
 6     
 7     foreach($elements as $element) {
 8         if (!$found && $this->isDesired($element, $desirability)) {
 9             $result = $element;
10             $found = true;
11         }
12     }
13     
14     return $found ? $result : null;
15 }

El operador ternario tiene bastantes problemas, pero, en general, es una buena solución cuando queremos expresar un cálculo que se resuelve de dos maneras según una condición. Eso sí: nunca anides operadores ternarios porque su lectura entonces se complica enormemente.

De todos modos, este ejemplo concreto de código puede mejorar mucho. Si nos fijamos, el primer elemento encontrado ya nos basta como respuesta, por lo que se podría devolver inmediatamente. No necesitamos ninguna variable temporal, ni una doble condición. Este tipo de refactor lo veremos en el capítulo sobre return early.

 1 function selectElement(Criteria $criteria, Desirability $desirability)
 2 { 
 3     foreach($this->getElements($criteria) as $element) {
 4         if ($this->isDesired($element, $desirability)) {
 5             return $element;
 6         }
 7     }
 8     
 9     return null;
10 }

Solo un if por método

Christian Clausen en Five Lines of Code propone un refactor para condicionales que puede ser muy interesante. Con frecuencia, una estructura condicional indica que una función está haciendo varias cosas distintas. Por tanto, lo que propone es que cada método haga solo una cosa, y que, por tanto, solo tenga un if que sería la primera línea. Si hay más de uno, separamos en métodos distintos.

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     
10     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
11 d() < 745) {
12         return new PaypalPaymentMethod();
13     }
14     
15     return new DefaultPaymentMethod();
16 }

Podría quedar más o menos así:

 1 function obtainPaymentMethod(Order $order): PaymentMethod {
 2     $selectedPaymentMethod = $order->getSelectedPaymentMethod();
 3     if ($selectedPaymentMethod !== null) {
 4         return $selectedPaymentMethod;
 5     }
 6     
 7     $logger = Logger::getInstance();
 8     $logger->debug("Medio de pago desconocido");
 9     
10     return obtainDefaultPaymentMethod($order);
11 }
12 
13 function obtainDefaultPaymentMethod(Order $order): PaymentMethod {
14      if ($order->getDestinationCountry() == Country::FRANCE && $order->\
15 id() < 745) {
16         return new PaypalPaymentMethod();
17      }
18     
19     return new DefaultPaymentMethod();
20 }

Resumen del capítulo

Las expresiones y estructuras condicionales pueden hacer que seguir el flujo de un código sea especialmente difícil, particularmente cuando están anidadas o son muy complejas. Mediante técnicas de extracción podemos simplificarlas, aplanarlas y hacerlas más expresivas.

Estas normas generales nos pueden ser útiles:

  • Poner la condición más corta primero.
  • Encapsular las condiciones en métodos o funciones para expresar la intención.
  • Encapsular las ramas en métodos o funciones que expresen la intención y, de paso, simplificar la estructura.
  • Evitar else cuando sea posible.
  • Expresar las condiciones de forma afirmativa.

Y volver, volver, volver…

En el que se trata un problema que viene del principio de los tiempos de la programación, cuando teníamos cosas como GOTO, números de línea y direcciones de memoria arbitrarias a las que saltar. Pero aún nos quedan algunos hábitos relacionados con esa idea. Aparte de eso, varios otros temas relacionados con el retorno de funciones y métodos.

En el blog ya hemos hablado del patrón clásico Single Exit Point y cómo acabó derivando en single return. También algún momento de esta guía de refactor hemos hablado también del return early. Ahora vamos a retomarlos conjuntamente porque seguramente nos los encontraremos más de una vez.

Notas de la segunda edición

Este capítulo cambia de lugar porque me he dado cuenta de que es un refactor relativamente menor y rompe por la mitad los dos capítulos dedicados a la distribución de responsabilidades.

Hasta este punto del libro estamos hablando de refactorings motivados por problemas o defectos del código que podemos identificar incluso visualmente, sin preocuparnos mucho de qué hace el código relacionado. Pero los capítulos siguientes ya requieren que pensemos en cuáles son los papeles que juegan los distintos objetos en el código.

Lo primero será saber de qué estamos hablando:

Single return

Se trata de que en cada método o función solo tengamos un único return, a pesar de que el código pueda tener diversos caminos que nos permitirían finalizar en otros momentos. Obviamente, si el método solo tiene un camino posible tendrá un solo return.

1 public function isValid(string $luhnCode) : bool
2 {
3     $inverted = strrev($luhnCode);
4 
5     $oddAdded = $this->addOddDigits($inverted);
6     $evenAdded = $this->addEvenDigits($inverted);
7 
8     return ($oddAdded + $evenAdded) % 10 === 0;
9 }

Si el método tiene dos caminos, caben dos posibilidades:

En la primera, uno de los flujos se separa del principal, hace alguna cosa y vuelve de forma natural al tronco para terminar lo que tenga que hacer.

 1 public function forProduct(Client $client, Product $product)
 2 {
 3     $contract = new Contract($product);
 4     
 5     if ($client->hasBenefits()) {
 6         $contract->addBenefits($client->benefits());
 7     }
 8     
 9     $this->mailer->send($client, $contract);
10 }

Uno de los flujos se separa para resolver la tarea de una manera alternativa, por lo que podría devolver el resultado una vez obtenido. Sin embargo, si se sigue el patrón single return, hay que forzar que el flujo vuelva al principal antes de retornar.

1 private function reduceToOneDigit($double) : int
2 {
3     if ($double >= 10) {
4         $double = intdiv($double, 10) + $double % 10;
5     }
6 
7     return $double;
8 }

Si el método tiene más de dos caminos se dará una combinación de las posibilidades anteriores, es decir, algunas ramas volverán de forma natural al flujo principal y otras podrían retornar por su cuenta.

En principio, la ventaja del Single Return es poder controlar con facilidad que se devuelve el tipo de respuesta correcta, algo que sería más difícil si tenemos muchos lugares con return. Pero la verdad es que explicitando return types es algo de lo que ni siquiera tendríamos que preocuparnos.

En cambio, el mayor problema que tiene Single Return es que puede forzar la anidación de condicionales y el uso de else hasta extremos exagerados, lo que provoca que el código sea especialmente difícil de leer. Lo peor es que eso no se justifica por necesidades del algoritmo, sino por la gestión del flujo para conseguir que solo se pueda retornar en un punto.

El origen de esta práctica podría ser una mala interpretación del patrón Single Exit Point de Djkstra, un patrón que era útil en lenguajes que permitían que las llamadas a subrutinas y sus retornos pudieran hacerse a líneas o posiciones de memoria arbitrarias, con la infaustamente famosa sentencia GOTO. El objetivo de este patrón era asegurar que se entrase a una subrutina en su primera línea y se volviese siempre a la línea siguiente a la llamada.

Early return

El patrón early return consiste en salir de una función o método en cuanto sea posible, bien porque se ha detectado un problema (fail fast), bien porque se detecta un caso especial que se maneja fuera del algoritmo general o por otro motivo.

Dentro de este patrón tenemos el caso particular de las cláusulas de guarda, que validan los parámetros recibidos y lanzan una excepción si no son correctos. También se encuentran aquellos casos particulares que necesitan un tratamiento especial, pero que es breve o inmediato.

De este modo, al final nos queda el algoritmo principal ocupando el primer nivel de indentación y sin elementos que nos distraigan.

El mayor inconveniente es la posible inconsistencia que pueda darse en los diferentes returns en cuanto al tipo o formato de los datos, algo que se puede controlar fácilmente forzando un return type.

Por otra parte, ganamos en legibilidad, ya que mantenemos bajo control el anidamiento de condicionales y los niveles de indentación. Además, al tratar primero los casos especiales podemos centrar la atención en el algoritmo principal de ese método.

Hagamos un ejemplo

Este es un código que escribí hace bastantes años para implementar el algoritmo Quicksort. El código visto ahora está un poco pobre, pero me viene muy al pelo para ilustrar como refactorizar retornos y hacer código un poco más fácil de mantener.

 1 class QuickSort
 2 {
 3     public function sort(array $source)
 4     {
 5         $length = count($source);
 6         if ($length > 1) {
 7             $pivot = $this->median($source);
 8             $equal = $less = $greater = [];
 9             for ($i = 0; $i < $length; $i++) {
10                 if ($source[$i] == $pivot) {
11                     $equal[] = $source[$i];
12                 } elseif ($source[$i] < $pivot) {
13                     $less[] = $source[$i];
14                 } else {
15                     $greater[] = $source[$i];
16                 }
17             }
18             $sorted = array_merge($this->sort($less), $equal, $this->so\
19 rt($greater));
20         } else {
21             $sorted = $source;
22         }
23 
24         return $sorted;
25     }
26 
27     private function median($source)
28     {
29         $points = [];
30         for ($i = 0; $i < 3; $i++) {
31             $point = array_splice($source, rand(0, count($source) - 1),\
32  1);
33             $points[] = array_shift($point);
34         }
35 
36         return array_sum($points) - max($points) - min($points);
37     }
38 }

El primer paso es invertir la condicional, para ver la rama más corta en primer lugar. Se aprecia claramente que cada una de las ramas implica una forma diferente de calcular la misma variable, que es lo que se va a devolver al final. El else se introduce porque no queremos que el flujo pase por el bloque grande si $source tiene un único elemento o ninguno, ya que no tendríamos necesidad de ordenarlo.

 1 public function sort(array $source)
 2 {
 3     $length = count($source);
 4     if ($length <= 1) {
 5         $sorted = $source;
 6     } else {
 7         $pivot = $this->median($source);
 8         $equal = $less = $greater = [];
 9         for ($i = 0; $i < $length; $i++) {
10             if ($source[$i] == $pivot) {
11                 $equal[] = $source[$i];
12             } elseif ($source[$i] < $pivot) {
13                 $less[] = $source[$i];
14             } else {
15                 $greater[] = $source[$i];
16             }
17         }
18         $sorted = array_merge($this->sort($less), $equal, $this->sort($\
19 greater));
20     }
21 
22     return $sorted;
23 }

Por esa razón, podríamos simplemente finalizar y retornar el valor de $source como que ya está ordenado cuando solo hay un elemento. Al hacer esto, también podemos eliminar el uso de la variable temporal $sorted que resulta innecesaria y suprimir la cláusula else porque ya hemos retornado en la primera rama.

 1 public function sort(array $source)
 2 {
 3     $length = count($source);
 4     if ($length <= 1) {
 5         return $source;
 6     }
 7 
 8     $pivot = $this->median($source);
 9     $equal = $less = $greater = [];
10     for ($i = 0; $i < $length; $i++) {
11         if ($source[$i] == $pivot) {
12             $equal[] = $source[$i];
13         } elseif ($source[$i] < $pivot) {
14             $less[] = $source[$i];
15         } else {
16             $greater[] = $source[$i];
17         }
18     }
19     
20     return array_merge($this->sort($less), $equal, $this->sort($greater\
21 ));
22 }

Con este arreglo el código ya mejora mucho su legibilidad gracias a que despejamos el terreno tratando el caso especial y dejando el algoritmo principal limpio.

Pero vamos a ir un paso más allá. El bucle for contiene una forma velada de single return en forma de estructura if...else que voy a intentar explicar.

El algoritmo quicksort se basa en hacer pivotar los elementos de la lista en torno a su mediana, es decir, al valor que estaría exactamente en la posición central de la lista ordenada. Para ello, se calcula la mediana de forma aproximada y se van comparando los números para colocarlos en la mitad que les toca: bien por debajo o bien por encima de la mediana.

Para eso se compara cada número con el valor mediano para ver sucesivamente si es igual, menor o mayor, con lo que se añade a la sublista correspondiente y se van ordenando esas sublistas de forma recursiva.

En este caso las cláusulas else tienden a hacer más difícil la lectura y, aunque la semántica es correcta, podemos hacerlo un poco más claro.

Como ya sabrás, podemos forzar la salida de un bucle con continue:

 1 public function sort(array $source)
 2 {
 3     $length = count($source);
 4     if ($length <= 1) {
 5         return $source;
 6     }
 7 
 8     $pivot = $this->median($source);
 9     $equal = $less = $greater = [];
10     for ($i = 0; $i < $length; $i++) {
11         if ($source[$i] == $pivot) {
12             $equal[] = $source[$i];
13             continue;
14         }
15 
16         if ($source[$i] < $pivot) {
17             $less[] = $source[$i];
18             continue;
19         }
20 
21         $greater[] = $source[$i];
22     }
23 
24     return array_merge($this->sort($less), $equal, $this->sort($greater\
25 ));
26 }

Y, aunque en este caso concreto no es especialmente necesario, esta disposición hace que la lectura del bucle sea más cómoda. Incluso es más fácil reordenarlo y que exprese mejor lo que hace:

 1 public function sort(array $source): array
 2 {
 3     $length = count($source);
 4     if ($length <= 1) {
 5         return $source;
 6     }
 7 
 8     $pivot = $this->median($source);
 9     $equal = $less = $greater = [];
10     for ($i = 0; $i < $length; $i++) {
11         if ($source[$i] > $pivot) {
12             $greater[] = $source[$i];
13             continue;
14         }
15 
16         if ($source[$i] < $pivot) {
17             $less[] = $source[$i];
18             continue;
19         }
20 
21         $equal[] = $source[$i];
22     }
23 
24     return array_merge($this->sort($less), $equal, $this->sort($greater\
25 ));
26 }

Cláusulas de guarda y alternativas

En el caso de las cláusulas de guarda, el early return se sustituye por el fail fast En este caso, se trata de validar los parámetros de entrada de un método o función y, si no son correctos, lanzar una excepción o devolver un valor por defecto.

1 public function add(int $a, int $b): int
2 {
3     if ($a < 0 || $b < 0) {
4         throw new InvalidArgumentException('Both numbers must be positi\
5 ve');
6     }
7 
8     return $a + $b;
9 }

Las cláusulas de guarda sirven para garantizar las precondiciones que requerimos para que el algoritmo se pueda ejecutar. Por ejemplo, en el caso anterior podríamos hacer algo como esto:

1 public functon assertPositive(int $a): void
2 {
3     if ($a < 0) {
4         throw new InvalidArgumentException('Number must be positive');
5     }
6 }

Y usarlo en la función add, en lugar de la estructura condicional.

1 public function add(int $a, int $b): int
2 {
3     assertPositive($a);
4     assertPositive($b);
5 
6     return $a + $b;
7 }

Una alternativa, por supuesto, sería usar objetos de valor para los números, que ya incluyan la validación de los valores.

 1 class PositiveNumber
 2 {
 3     private $value;
 4 
 5     public function __construct(int $value)
 6     {
 7         if ($value < 0) {
 8             throw new InvalidArgumentException('Number must be positive\
 9 ');
10         }
11 
12         $this->value = $value;
13     }
14 
15     public function value(): int
16     {
17         return $this->value;
18     }
19     
20     public function add(PositiveNumber $number): PositiveNumber
21     {
22         return new PositiveNumber($this->value() + $number->value());
23     }
24 }

Variables temporales para retornar cosas

Una variable temporal es aquella que usamos para mantener un valor que hemos calculado hasta que le damos uso. En algunos casos, una variable temporal puede ir acumulando cálculos, pero en otros casos simplemente se usa para mantener un valor que se va a devolver inmediatamente. Esta es una situación típica:

1 public function add(int $a, int $b): int
2 {
3     $result = $a + $b;
4 
5     return $result;
6 }

En este caso, la variable $result no aporta nada al código y se puede eliminar sin problemas.

1 public function add(int $a, int $b): int
2 {
3     return $a + $b;
4 }

El único inconveniente que tiene este refactor es que puede hacer que la depuración se complica un poco, pues si pones un punto de detención en la línea del return no podrás ver el valor que se va a devolver. Pero, en general, es un cambio que no afecta al código y que mejora la legibilidad, evitando introducir un símbolo que se desecha inmediatamente.

Resumen del capítulo

El patrón single return es una práctica que consiste en que cada método o función tenga un único punto de salida. Por desgracia, esto puede llevar a anidar condicionales y a hacer el código más difícil de leer.

Como alternativa, podemos refactorizar el código para que retorne lo antes posible, permitiendo que el algoritmo principal esté en el primer nivel de indentación y que los casos especiales se manejen en primer lugar. En conjunto, el código quedará más fácil de leer y de mantener.

En el caso de las cláusulas de guarda, el early return se sustituye por el fail fast En este caso, se trata de validar los parámetros de entrada de un método o función y, si no son correctos, lanzar una excepción o devolver un valor por defecto.

La paradoja de las colecciones

Capítulo que trata sobre encapsular estructuras de datos que representan colecciones de elementos en objetos que, paradójicamente, no tienen que ser colecciones en sentido estricto.

Las reglas de Object Calisthenics nos piden encapsular todas las estructuras de datos que representan alguna colección en objetos. Pero, en contra de lo que nos sugiere el nombre, estos objetos no tienen por qué ser en sí mismos colecciones. De hecho, no es especialmente recomendable que lo sean. Más bien, al contrario.

Notas de la segunda edición

Este capítulo es nuevo es esta edición.

Abstraer las estructuras de datos

Creo que fue leyendo el libro de Sandi Metz, “Practical Object-Oriented Design in Ruby”, que me llamó la atención la idea de ocultar las estructuras de datos incluso dentro de la misma clase. Es decir, si una clase tiene un atributo que es un array, no deberíamos acceder a su contenido directamente desde dentro de la clase, sino que deberíamos hacerlo a través de métodos.

 1 class Order
 2 {
 3     private $items = [];
 4 
 5     public function addItem(Item $item): void
 6     {
 7         $this->items[] = $item;
 8     }
 9     
10     public function total(): float
11     {
12         $total = 0;
13         foreach ($this->items as $item) {
14             $total += $item->price();
15         }
16         return $total;
17     }
18     
19     private function item($position int): float
20     {
21         return $this->items[$position];
22     }
23    
24 }

Esta idea es contra-intuitiva, porque nos parece lógico acceder directamente a los atributos de una clase desde dentro de ella. Pero, si lo pensamos bien, hacerlo tiene algunos problemas:

  • Para usar la estructura de datos tenemos que saber cosas sobre ella, como por ejemplo qué tipo de estructura es. Aquí es un array, pero podría ser un diccionario, una lista enlazada, un conjunto, etc. Si accedemos a la estructura de datos directamente, estamos acoplando el código que la usa a la estructura de datos concreta. Es un caso de Inappropriate Intimacy.
  • Si cambiamos la estructura de datos, tendremos que cambiar el código que la usa, lo que puede suponer un caso de Shotgun Surgery, ya que probablemente la estamos usando en muchos lugares y necesitaremos modificar todos esos usos.

Podemos evitar ambos problemas encapsulando la estructura de datos en métodos. De esta forma, si usamos otra, solo tenemos que cambiar los métodos que la encapsulan, y no el código que la usa. Lo mejor es que el objeto envoltorio puede ser cualquier cosa, no tiene por qué ser una colección. Nos basta con que responda a los mensajes que necesitemos y no es necesario implementar cada uno de los métodos que caracterizan a la mayor parte de estructuras de datos. Fíjate en el ejemplo a continuación como el método averagePrice() no tiene ni idea de la estructura subyacente.

 1 class Items
 2 {
 3     private $items = [];
 4 
 5     public function addItem(Item $item): void
 6     {
 7         $this->items[] = $item;
 8     }
 9     
10     public function averagePrice(): float
11     {
12         return $this->total() / $this->count();
13     }
14     
15     public function total(): float
16     {
17         $total = 0;
18         foreach ($this->items() as $item) {
19             $total += $item->price();
20         }
21         return $total;
22     }
23     
24     private function item($position int): float
25     {
26         return $this->items[$position];
27     }
28     
29     private function items(): array
30     {
31         return $this->items;
32     }
33     
34     private function count(): int
35     {
36         return count($this->items);
37     }
38 }

Tanto es así que los consumidores de Items no tienen ni por qué saber que se trata de una colección. Es tan solo un objeto al que podemos enviarle el mensaje total() y nos devolverá el total de los elementos que contiene.

 1 class Order
 2 {
 3     private Items $items;
 4 
 5     public function __construct(Items $items)
 6     {
 7         $this->items = $items;
 8     }
 9     
10     public function total(): float
11     {
12         return $this->items->total();
13     }
14 }   

Además del beneficio de darnos libertad para cambiar la estructura primitiva de datos sin afectar a los otros objetos consumidores, también podemos encapsular comportamiento en ese objeto. Es el caso del cálculo del total de los elementos de la colección, que hemos movido a la clase Items.

Esto ejemplifica que la clase consumidora de Items, que en nuestro ejemplo es Order, no necesita acceder a sus elementos individualmente, porque solo está interesada en agregaciones de sus datos, así que no necesita que sea una colección. En consecuencia, se simplifica el desarrollo de Order, permitiéndonos pensar en un nivel de abstracción más alto y despreocuparnos de muchos detalles. Un buen ejemplo de Tell, don’t ask, un principio de diseño que desarrollaremos en un capítulo posterior.

Por qué las colecciones no tienen que ser colecciones

Los lenguajes de programación ofrecen diversas estructuras de datos con las que representar colecciones de elementos, tales como arrays, listas, diccionarios, conjuntos, colas, pilas, etc. Cada una de estas estructuras es adecuada para distintas finalidades, y cada una tiene sus propias propiedades y comportamientos. Por ejemplo, una pila es una estructura en la que el último elemento que ha entrado es el primero en salir, mientras que en una cola es el primero en entrar el primero en salir. Un array, típicamente nos permite acceder a sus elementos por su posición, mientras que en un diccionario podemos acceder a ellos por una clave.

Ante un caso de uso concreto deberíamos utilizar la estructura que mejor se ajuste a nuestras necesidades. Puesto que esto no siempre está claro desde el principio, a veces necesitamos cambiar esta estructura. Por otro lado, cada estructura nos presenta una serie de comportamientos que no tienen la semántica adecuada a nuestro caso de uso, ni vamos a utilizar todos ellos. Esto conlleva un grado de acoplamiento entre la estructura de datos y el código que la usa, que puede ser problemático, pero que podemos evitar encapsulando la colección en un objeto con una interfaz más adecuada.

Vamos a entenderlo mejor con un ejemplo. Supongamos una aplicación de seguimiento de tareas. Podríamos tener una clase Task, como esta:

 1 class Task
 2 {
 3     private $status;
 4     private $startDate;
 5     private $doneDate;
 6 
 7     public function __construct($status, $startDate, $doneDate = null)
 8     {
 9         $this->status = $status;
10         $this->startDate = $startDate;
11         $this->doneDate = $doneDate;
12     }
13 
14     public function status()
15     {
16         return $this->status;
17     }
18 
19     public function start()
20     {
21         $this->status = 'in progress';
22         $this->startDate = new DateTime();
23     }
24 
25     public function started()
26     {
27         return $this->startDate;
28     }
29     
30     public function finish()
31     {
32         $this->status = 'done';
33         $this->doneDate = new DateTime();
34     }
35 }

Ahora, imaginemos este servicio que gestiona la colección de tareas, en este caso para decirnos las tareas que están pendientes de terminar:

 1 class TaskService
 2 {
 3     private $tasks;
 4 
 5     public function __construct(array $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function getPendingTasks(): array
11     {
12         $pendingTasks = [];
13         foreach ($this->tasks as $task) {
14             if ($task->status() === 'in progress') {
15                 $pendingTasks[] = $task;
16             }
17         }
18         return $pendingTasks;
19     }
20 }

¿Eres capaz de ver el problema? Tal como está programado, TaskService tiene que saber muchas cosas acerca de la estructura de datos que representa la colección de tareas: que es un array, que tiene que recorrerlo entero, etc. De hecho, tiene que saber cosas de Task, y ese es otro problema de acoplamiento. Si cambiamos la estructura de datos, o la forma de acceder a los elementos, o la forma de filtrarlos, tendríamos que cambiar TaskService, lo que sería un caso de Shotgun Surgery.

Podríamos cambiar la implementación para adoptar un método más funcional, usando array_filter, encapsulando la lógica de filtrado en una función anónima. El resultado es un código más conciso. Pero, aun así, TaskService sigue teniendo el mismo acoplamiento y se ve obligado a aportarle comportamiento a la estructura de datos y a mantener conocimiento sobre ella.

 1 class TaskService
 2 {
 3     private array $tasks;
 4 
 5     public function __construct(array $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function getPendingTasks(): array
11     {
12         return array_filter($this->tasks, function ($task) {
13             return $task->status() === 'in progress';
14         });
15     }
16 }

Nosotras lo que queremos es que TaskService sepa lo mínimo necesario. Así, lo único que debería saber TaskService sobre $tasks es que es un objeto al que le puede preguntar por las tareas pendientes. No debería saber que están organizadas en un array, ni la forma en que se filtran las tareas pendientes. De hecho, no debería saber ni siquiera que es una colección. Solo debería saber que es un objeto que responde a un mensaje pendingTasks() con otra colección que solo incluye las tareas en progreso.

 1 class TaskCollection
 2 {
 3     private array $tasks;
 4 
 5     public function __construct(array $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function pendingTasks(): TaskCollection
11     {
12         $inProgress = array_filter($this->tasks, function ($task) {
13             return $task->status() === 'in progress';
14         });
15         
16         return new TaskCollection($inProgress) ;
17     }
18 }

Ahora, TaskService solo tiene que saber que TaskCollection tiene un método pendingTasks(). Si cambiásemos la estructura de datos subyacente por un hash, o cambiamos la forma de filtrar las tareas pendientes, o cualquier otra cosa, TaskService seguiría funcionando igualmente. El resultado es que TaskService se simplifica hasta el punto de tener una lógica trivial. La complejidad de la lógica de filtrado se ha trasladado a TaskCollection, que es donde debería estar.

 1 class TaskService
 2 {
 3     private TaskCollection $tasks;
 4 
 5     public function __construct(TaskCollection $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function pendingTasks(): TaskCollection
11     {
12         return $this->tasks->pendingTasks();
13     }
14 }

Como puedes ver TaskCollection representa una colección, pero en sí misma no es una colección. En lugar de exponer comportamientos genéricos de una colección, lo que nos presenta son métodos adecuados al dominio de la aplicación. En este caso, pendingTasks(), que es un concepto importante en una aplicación de gestión de tareas. A esto nos referíamos al principio cuando decíamos que las colecciones no tienen por qué ser colecciones, aunque se implementen usando estructuras de datos de colección.

Beneficios de la encapsulación de colecciones

Ahora, imaginemos que queremos implementar una funcionalidad pickNext. La idea es que nos entrega la siguiente tarea que deberíamos estar realizando, para evitarnos tener que decidir. Al principio podríamos implementarlo simplemente definiendo la próxima tarea como la que haya entrado primero en la lista. Esto nos sugiere usar una cola. En PHP no tenemos colas nativas, pero podemos usar un array como cola. La función array_shift sirve para sacar el primer elemento de la cola.

 1 class TaskCollection
 2 {
 3     private array $tasks;
 4 
 5     public function __construct(array $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function pendingTasks(): TaskCollection
11     {
12         $inProgress = array_filter($this->tasks, function ($task) {
13             return $task->status() === 'in progress';
14         });
15         
16         return new TaskCollection($inProgress) ;
17     }
18     
19     public function pickNext(): Task
20     {
21         return array_shift($this->tasks);
22     }
23 }

Ahora bien, más adelante queremos añadir una prestación con la que asignar prioridades a las tareas y, por tanto, cambiar el modo en que se seleccionan. Esto introduce nuevas reglas de negocio, que podríamos definir:

  • La próxima tarea será la de prioridad más alta
  • En casa de que haya varias tareas con la misma prioridad, se escogerá la que lleve más tiempo

Así que tenemos dos reglas que no cooperan mucho entre ellas. Ya no nos basta con utilizar una estructura de cola. Pero no es problema: al estar encapsulado, el código consumidor solo tiene que saber que el mensaje pickNext devolverá la Task adecuada según las nuevas reglas. En el siguiente ejemplo, hemos usado usort para ordenar las tareas según las reglas de negocio que hemos definido.

 1 class TaskCollection
 2 {
 3     private array $tasks;
 4 
 5     public function __construct(array $tasks)
 6     {
 7         $this->tasks = $tasks;
 8     }
 9 
10     public function pendingTasks(): TaskCollection
11     {
12         $inProgress = array_filter($this->tasks, function ($task) {
13             return $task->status() === 'in progress';
14         });
15         
16         return new TaskCollection($inProgress) ;
17     }
18     
19     public function pickNext(): Task
20     {
21         usort($this->tasks, function ($a, $b) {
22             if ($a->priority() === $b->priority()) {
23                 return $a->added() <=> $b->added();
24             }
25             return $a->priority() <=> $b->priority();
26         });
27         
28         return array_shift($this->tasks);
29     }
30 }

Resumen

Encapsular colecciones en objetos nos permite ocultar los detalles de implementación y las estructuras de datos, exponiendo solo comportamientos que sean importantes para los consumidores de esos objetos. Esto nos da libertad para cambiar la estructura de datos sin afectar a los objetos consumidores, y nos permite encapsular comportamiento en esos objetos, simplificando el desarrollo y reduciendo la fuerza del acoplamiento.