La guía del refactor cotidiano
La guía del refactor cotidiano
Fran Iglesias
Buy on Leanpub

Introducción a la segunda edición

Hace ya unos cuantos años que escribí Refactor Cotidiano como una especie de guía sobre cuando y qué refactorizar en el trabajo diario como desarrolladora.

Cuando comencé a trabajar profesionalmente en el mundo de la programación, la idea de refactorizar no me resultó nunca extraña. Como persona que llevaba escribiendo blogs y otros textos desde años atrás, y viniendo del mundo educativo, estaba muy habituada a la idea de revisar y refinar todos los textos que escribía. Se convierte en algo natural, tanto para evitar en lo posible faltas de ortografía o sintaxis, como para asegurar que lo escrito refleja correctamente tu pensamiento y puede generar el efecto que pretendes cuando sea leído.

Por esa razón, mi estilo de programación se parece mucho al proceso que seguía y sigo al escribir. Expreso una idea en código y trato de refinarla en sucesivas revisiones, las cuales me permiten entender mejor lo que estoy haciendo y me abren oportunidades para otras mejoras.

A veces, resulta difícil encontrar la forma adecuada de expresar un concepto mediante código. En ese caso, intento hacerlo de forma basta, aunque funcional. Una vez que lo tengo delante, es cuando puedo examinarlo en busca de ciertos patrones que me dan las pistas de cómo modificarlo para que la expresividad y el diseño sean mejores.

Esto tiene un límite. Muchas veces ocurre que mi conocimiento actual del dominio en el que esté trabajando no es suficiente para expresarlo de la mejor manera. Y en esos casos, el código va así a producción: es funcional, hace lo que se supone que debe hacer, pero es manifiestamente mejorable. No pretendo seguir dando más vueltas hasta encontrar la versión perfecta. Esas mejoras llegarán en un futuro, cuando una nueva intervención nos lleve por esos mismos archivos, pero esta vez con un mejor conocimiento.

Acerca de este libro

Refactor cotidiano no es un catálogo de técnicas al estilo del Refactoring de M. Fowler, sino más bien una colección de ideas sobre temas o áreas en las que fijarse cuando estamos trabajando.

Por ejemplo, el libro empieza hablando de los comentarios y discute algunos de los problemas que pueden aparecer relacionados con ellos, cuando usarlos y cómo hacer para que sean innecesarios.

Acerca de la segunda edición

Refactor cotidiano tiene el honor de haber sido el libro de la primera edición de la PulpoCon (2019). Esta conferencia dedicada al desarrollo de software con orientación crafter se celebra en Vigo, durante algún fin de semana de septiembre, y ya va por la cuarta edición a la hora de escribir estas líneas.

El caso es que la conferencia adoptó la costumbre de regalar un libro exclusivo a las personas asistentes y cuando Rolando Caldas, su organizador, me comentó la idea, yo tenía el material ya publicado en formato digital y podría convertirse fácilmente en ese libro físico.

Siempre tuve la sensación de que no era ninguna maravilla, pero por lo visto resultó útil a muchas personas, de lo cual me alegro. El caso es que poco después de la celebración de la edición de 2023 me planteé revisar el libro.

Al releerlo me sorprendió que no había envejecido tan mal como pensaba, salvo por los ejemplos de código, que son de PHP y de una versión relativamente antigua. Pero, por otro lado, se me ocurrió que tal vez se podrían ampliar algunos capítulos, retirar las alusiones al blog, mejorar los ejemplos, tal vez introducir otros lenguajes, y convertir el libro en un manual un poco más moderno y más transversal.

Para ser franco, había varios ejemplos que no me gustan ahora, algunas soluciones que no aplicaría, y bastantes cosas que no están explicadas de la mejor manera… Cosas de la falta de experiencia.

Siendo un poco más sistemático, he aquí las áreas de mejora del libro:

  • En algunos capítulos se remite a la lectora a páginas del blog para ampliar la explicación del porqué de algunos refactors. Esta explicación podría incorporarse en el capítulo para completarlos, aunque sea de forma resumida.
  • El capítulo de enumerables es un poco específico de la situación de PHP en el momento de escribirlo. El lenguaje ahora tiene la suya propia, más similar a la de otros lenguajes. Diría que este capítulo necesita una reescritura más profunda que refleje la nueva realidad y que aproveche los puntos positivos de la propuesta original del libro.
  • Se echa de menos el tema del refactor a colecciones, lo que merece posiblemente su propio capítulo.
  • Otro asunto que tiene más que ver con el enfoque es: ¿por qué no estructurar el libro en torno a problemas frecuentes del código y cómo abordarlos mediante refactor? Algo similar a los code smells, pero quizá expresados de una manera menos taxonómica.
  • Revisar el lenguaje y la expresión, aclarando algunos párrafos confusos.

Así que con estas ideas en la cabeza, empecé a trabajar, intercalando con otros proyectos. A medida que iba tomando forma, surgieron otras ideas más:

  • Algunos cambios en la organización del índice
  • Incluir un capítulo para sugerencias de lecturas y otros recursos
  • Una pequeña introducción a cada capítulo, de estilo pretendidamente humorístico
  • Notas a la segunda edición en cada capítulo, explicando los cambios introducidos en el capítulo cuando van más allá de la revisión de estilo o expresión.

Introducción de la primera edición

En The Talking Bit hemos escrito bastante sobre refactor, principalmente porque nos parece que es una de las mejores cosas que podemos hacer con nuestro código, sea nuevo o legacy. El refactor es una forma de mantenimiento del código que consiste en mejorar su expresividad a través de pequeños cambios que no alteran el comportamiento y que tampoco cambian sustancialmente la implementación.

Una reescritura, por el contrario, suele plantear un cambio brusco de implementación que podría incluso provocar cambios en el comportamiento. Este proceso busca reconstruir el software a partir de nuevos principios o ideas.

Por otro lado, el refactor puede hacerse de una manera continua e iterativa, interviniendo en el código siempre que se tenga ocasión. Por ejemplo: porque estamos revisándolo a fin de corregir un error o añadir una nueva característica.

Durante el proceso de lectura y análisis podemos encontrarnos con fragmentos de código que no expresan bien un concepto del dominio, que no se entienden fácilmente o que interfieren en la compresión de ese código. Ese momento es ideal para realizar pequeños refactors seguros que, acumulados a lo largo del tiempo, van haciendo que el código evolucione hacia un mejor diseño. Pero sobre todo, hacen que el código refleje cada vez mejor el conocimiento que tenemos.

Como ya hemos mencionado en otras ocasiones, el refactor trata principalmente sobre conocimiento y significado. Es decir, trata sobre que el código exprese cosas y, concretamente, que exprese de la mejor forma posible nuestro conocimiento sobre el dominio en el que trabajamos y cómo estamos resolviendo los problemas que nos plantea.

Por esa razón, se nos ha ocurrido que podría ser buena idea crear una especie de curso o guía para aprender sobre cómo hacer refactor cotidiano.

Refactor sin piedad

El refactor cotidiano es otro nombre para una práctica de Extreme Programming conocida como refactor sin piedad. Dicho en pocas palabras, consiste en refactorizar el código en cualquier momento en que sea posible. Contra lo que podría parecer no se trata de engolfarse dando vueltas al código hasta llegar a un código perfecto, sino de realizar retoques limitados para corregir lo que en ese momento podemos considerar como pequeñas imperfecciones. Por ejemplo:

  • Un nombre no consigue expresar correctamente un concepto.
  • Una expresión compleja podría descomponerse otras más pequeñas, más fáciles de entender y mantener.
  • El cuerpo de un método o función es muy largo y puede ser dividido en secciones autónomas.
  • Una expresión condicional es difícil de entender y se puede encapsular como función o método con un nombre expresivo, ocultando sus detalles.
  • Una estructura condicional presenta demasiada anidación, de modo que extraemos sus partes para organizarlo según niveles de abstracción.
  • Un fragmento de código está en lugar inadecuado y lo movemos a donde corresponde.

Con el tiempo, la acumulación de estos pequeños cambios irá mejorando la estructura, expresividad y mantenibilidad del código. Es muy posible que acabe revelando oportunidades de cambio de mayor calado que tal vez impliquen un rediseño.

En cualquier caso, al mejorar la situación del código, mejoran nuestras posibilidades de intervenir en él. Incorporar nuevas prestaciones será más rápido y seguro, lo que nos permite entregar con mayor frecuencia y predictibilidad.

Los momentos del refactor

En su charla Flows of refactoring, Martin Fowler explica qué es realmente el refactor y cuándo se tendría que hacer.

Fowler insiste en la idea de que si necesitas un momento especial para refactorizar es que no estás haciendo refactoring, sino, tal vez, reescribiendo o cambiando el diseño de tu software.

Existen tres momentos principales para el refactor:

  • Durante la lectura de código
  • Refactor preparatorio para introducir un cambio
  • Refactor posterior a un cambio

Lectura de código, los momentos WTF

Pasamos la mayor parte del tiempo leyendo código. Cada vez que queremos introducir un cambio necesitamos leer el código existente para saber dónde es el mejor lugar para hacerlo o averiguar qué recursos ya están disponibles. También leemos el código simplemente para buscar algún tipo de conocimiento relacionado con el código mismo o el negocio.

En esos momentos podemos encontrarnos con fragmentos que nos hagan lanzar una interjección (WTF), o que necesitamos leer dos o tres veces para entender lo que ocurre. Momentos en los que vemos algo que, por alguna razón no cuadra. No es que no funcione, es simplemente que hay algo descolocado, sucio, fuera de sitio, desentonando. En una palabra: desordenado.

Por lo general, estos momentos de extrañeza los genera la existencia de una distancia entre nuestro conocimiento del negocio y cómo se expresa en el propio código. Este desfase es lo que Ward Cunningham denominó deuda técnica.

La deuda técnica se paga refactorizando el software para que refleje el conocimiento del negocio de la mejor manera posible.

En cierto modo, es imposible evitar que el código tenga un cierto nivel de deuda técnica porque el conocimiento del negocio cambia mucho más rápidamente. De hecho, cuando introducimos cambios en el software y los llevamos a producción, el negocio ya está cambiando. En parte, gracias a las nuevas funcionalidades del software y al impacto que tienen.

La forma de prevenir el crecimiento de la deuda técnica es entregar de forma sostenida pequeños cambios y refactorizar constantemente.

Refactor preparatorio

Al introducir cambios en el código para incorporar nuevas funcionalidades o corregir errores suele ser necesario arreglar el código existente para hacer posibles esos mismos cambios. Una cita de Kent Beck lo expresa más o menos así:

For each desired change, make the change easy (warning: this may be hard), then make the easy change

Es decir:

Primero, haz que el cambio sea fácil (advertencia: puede ser difícil conseguirlo). Luego haz el cambio [que ahora es] fácil.

Con frecuencia el código no está preparado para admitir ciertos cambios, por lo que debemos transformarlo primero a fin de que la nueva funcionalidad sea fácil de introducir.

Esto es lo que denominamos refactor preparatorio y debería hacerse siempre antes de introducir nuevas funcionalidades. En otras palabras: mejoramos la estructura del código asegurándonos de no cambiar su comportamiento. Mezclamos esos cambios y solo entonces procedemos a desarrollar la nueva funcionalidad.

Usando otra metáfora de Kent Beck: nos ponemos dos sombreros: el de refactorizar y el de crear nueva funcionalidad. No podemos llevar los dos sombreros a la vez.

Refactor posterior a un cambio

Tras realizar los cambios necesarios volvemos a encasquetarnos el sombrero de refactor para limpiar el código que acabamos de introducir.

Es posible que nuestra nueva funcionalidad haya introducido algo de duplicación y esto podría revelar la existencia de un concepto más general que hasta entonces no habíamos descubierto. En otras ocasiones, el código nuevo incrementa la complejidad de algún área y debemos trabajar para simplificarla.

Al añadir código a un sistema de software, aumentamos su entropía o desorden. La entropía de software es un concepto introducido por Ivar Jacobson y otros en el libro Object-Oriented Software Engineering: A Use Case Driven Approach. La mejor forma de luchar contra un crecimiento desmesurado de la entropía, es aplicar un refactor continuado, particularmente tras introducir código nuevo.

Cómo refactorizar

La idea del refactor cotidiano es muy simple: Se trata de realizar pequeños cambios inocuos en nuestro código en cualquier momento que se nos presente la ocasión. Es lo que algunos autores denominan refactor oportunista. Nuestra propuesta concreta es que hagas un refactor muy pequeño cada vez que lo veas necesario, de modo que, en una primera fase:

  • solo tocas un archivo.
  • los cambios quedan recogidos en un único commit atómico, que contengan solo los cambios debidos a ese refactor.

En una segunda fase, a medida que ganas confianza:

  • los cambios podrían afectar a varios archivos, pero el ámbito es limitado.
  • los cambios quedan recogidos en un único commit atómico.

En una tercera fase:

  • los cambios podrían suponer introducción de nuevas clases.
  • de nuevo, los cambios quedarían recogidos en un único commit.

La guía

Esta guía se compone de una serie de capítulos en los que se exponen diversas orientaciones y principios que seguir a la hora de refactorizar. La idea es explicar ámbitos en los que podrías intervenir en los tres niveles indicados en el apartado anterior. En muchos casos los refactors propuestos, al menos en el primer nivel o fase no necesitarían tests porque podrían ejecutarse mediante herramientas del IDE.

Con el tiempo, es posible que esos pequeños refactors, acumulados día tras día, mejoren la forma y calidad de tu código y te despejen caminos para mejorar su expresividad y arquitectura.

Así que, ¡happy tiding!

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.

Refactor y rediseño

En los siguientes capítulos hablaremos de acciones de refactor de mayor calado. Aquí ya no se trata de clarificar el código, sino de reorganizar el conocimiento que representa, aplicando mejores principios y patrones de diseño y arquitectura.

Dónde poner el conocimiento

En el que recurrimos a principios básicos de asignación de responsabilidades para averiguar qué objetos deberían saber qué cosas.

Notas de la segunda edición

En este capítulo introduciremos los principios GRASP, los cuales habíamos mencionado en la edición anterior, por lo que cambiamos casi por completo los ejemplos. Por otro lado, hemos eliminado la sección sobre el patrón Specification, que resulta demasiado sofisticado como para ser útil en el contexto de refactoring que estamos tratando.

Refactorizar para trasladar conocimiento

Solemos decir que refactorizar tiene que ver con el conocimiento y el significado. Fundamentalmente, porque lo que hacemos es aportar significado al código con el objetivo de que este represente de una manera fiel y dinámica el conocimiento cambiante que tenemos del negocio del que nos ocupamos. Y, por otro lado, porque lo que perseguimos con un refactoring continuado es representar nuestro entendimiento actual del negocio en el código con la mayor precisión posible.

En el código de una aplicación tenemos objetos que representan alguna de estas cosas:

  • Conceptos, ya sea en forma de entidades o de value objects. Las entidades representan conceptos que nos interesan por su identidad y tienen un ciclo de vida. Los value objects representan conceptos que nos interesan por su valor.
  • Relaciones entre esos conceptos, que suelen representarse en forma de agregados y que están definidas por las reglas de negocio.
  • Procesos que hacen interactuar los conceptos conforme a reglas de negocio también.

Uno de los problemas que tenemos que resolver al escribir código y al refactorizarlo es dónde poner el conocimiento y, más exactamente, las reglas de negocio.

Si hay algo que caracteriza al legacy es que el conocimiento sobre las reglas de negocio suele estar disperso a lo largo y ancho del código, en los lugares más imprevisibles y representado de las formas más dispares. El efecto de refactorizar este código es, esperamos, llegar a trasladar ese conocimiento al lugar donde mejor nos puede servir.

Pero incluso en código nuevo, el conocimiento puede estar disperso. Puede ser debido a que no conocemos bien nuestro negocio todavía, o porque no somos capaces de expresarlo mejor en un momento dado. Además, el código siempre va a tener un cierto desfase con lo que sabemos del negocio, porque el negocio cambia y nuestro conocimiento de él también.

Para saber donde colocar el conocimiento en el código podemos recurrir a varios principio y patrones.

Principios básicos

Principio de abstracción. Benjamin Pierce formuló el principio de abstracción en su libro Types and programming languages:

Each significant piece of functionality in a program should be implemented in just one place in the source code. Where similar functions are carried out by distinct pieces of code, it is generally beneficial to combine them into one by abstracting out the varying parts.

DRY. Por su parte, Andy Hunt y David Thomas, en The Pragmatic Programmer, presentan una versión de este mismo principio que posiblemente te sonará más: Don’t Repeat Yourself:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

En esencia, la idea que nos interesa recalcar es que cada regla de negocio estará representada en un único lugar y esa representación será la de referencia para todo el código.

Los principios que hemos enunciado se centran en el carácter único de la representación, pero no nos dicen dónde debe residir la misma. Lo cierto es que es un tema complejo, pues es algo que puede admitir varias interpretaciones y puede depender del estado de nuestro conocimiento actual del negocio.

Buscando dónde guardar el conocimiento: patrones GRASP

Los patrones GRASP son un conjunto de patrones de diseño que nos ayudan a asignar responsabilidades a los objetos de un sistema. Fueron introducidos por Craig Larman en Applying UML and Patterns, que identifica una serie de preguntas que nos podemos hacer para saber dónde colocar el conocimiento en un sistema orientado a objetos.

Regla general: en los objetos que tienen la información necesaria

Este patrón se llama Information Expert y es el más general de todos. Una responsabilidad se asignará al objeto que tenga la información necesaria para ejercerla.

En el contexto de refactoring, lo que nos dice este principio es que cuando estamos usando la información contenida en un objeto, ese uso o comportamiento tendría que estar en ese mismo objeto. Expresándolo en otras palabras, quiere decir que un objeto debe ser capaz de realizar todos los comportamientos que le sean propios, dentro del contexto de nuestra aplicación. Para ello no debería necesitar exponer sus propiedades internas o estado.

Por tanto, cuando preguntamos a un objeto sobre su estado y realizamos acciones basadas en la respuesta, lo suyo debería ser encapsular esas acciones en forma de comportamientos del objeto. Para ello, podemos seguir el principio Tell, don’t ask. Esto es, en lugar de obtener información de un objeto para operar con ella y tomar una decisión sobre ese objeto, le pedimos que lo haga él mismo y nos entregue un resultado si es adecuado para el contexto. Esto lo trataremos con más detalle en el siguiente capítulo.

Los value objects y entidades son lugares ideales para encapsular conocimiento de dominio.

Supongamos que en nuestro negocio estamos interesados en ofrecer productos o ventajas a usuarios cuya cuenta de correo pertenezca a ciertos dominios. Un ejemplo de esto son los programas de beneficios de algunas empresas. El correo electrónico es, pues, un concepto importante del negocio y lo representamos mediante un value object:

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

En un momento dado nos puede interesar saber si un empleado tiene acceso a un beneficio concreto, cosa que vamos a controlar obteniendo la lista de dominios corporativos que lo ofrecen. Podríamos hacerlo de esta manera:

 1 class CanSeeBenefit
 2 {
 3     public function byCorporateEmail(Email $email)
 4     {
 5         [, $domain] = explode('@', (string)$email);
 6 
 7         if (!in_array($domain, $this->getBenefitDomains->execute(), tru\
 8 e)) {
 9             return false;
10         }
11         
12         return true
13     }
14 }

Como se puede ver, estamos pidiendo su valor a $email para poder extraer el dominio y compararlo con la lista de dominios. Por definición, sabemos que un email se compone de un nombre de usuario y un dominio, así que lo lógico sería preguntarle a $email por su dominio y no calcularlo fuera de él.

 1 class Email
 2 {
 3     private string $email;
 4 
 5     public function __construct(string $email)
 6     {
 7         $this->email = $email;
 8     }
 9     
10     public static function valid(string $email)
11     {
12         if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
13             throw new InvalidArgumentException(sprintf('%s is not valid\
14  email.', $email));
15         }
16         return new self($email)
17     }
18 
19     public function domain(): string
20     {
21         [, $domain] = explode('@', $this->email);
22 
23         return $domain;
24     }
25 
26     public function __toString(): string
27     {
28         return $this->$email;
29     }
30 }

Este es un primer paso para trasladar el conocimiento al lugar donde mejor se puede usar.

 1 class CanSeeBenefit
 2 {
 3     public function byCorporateEmail(Email $email)
 4     {
 5         if (!in_array($email->domain(), $this->getBenefitDomains->execu\
 6 te(), true)) {
 7             return false;
 8         }
 9         
10         return true
11     }
12 }

Pero en el fondo esto no soluciona completamente el problema. Ahora podemos obtener el dominio de un email y, aunque se obtenga de un cálculo, no deja de ser el acceso a una propiedad. Cierto es que lo hemos implementado de tal forma que necesitamos calcular el dominio, pero podría no ser así. Mira, por ejemplo, esta versión:

 1 class Email
 2 {
 3     private string $username;
 4     private string $domain;
 5 
 6     private function __construct(string $username, string $domain)
 7     {
 8         $this->username = $username;
 9         $this->domain = $domain;
10     }
11 
12     public static function valid(string $email): self
13     {
14         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
15             throw new InvalidArgumentException(sprintf('%s is not a val\
16 id email.', $email));
17         }
18 
19         [$username, $domain] = explode('@', $email);
20         return new self($username, $domain);
21     }
22 
23     public function domain(): string
24     {
25         return $this->domain;
26     }
27 
28     public function __toString(): string
29     {
30         return $this->username . '@' . $this->domain;
31     }
32 }

Entonces, ¿qué podríamos hacer? Pues la respuesta es invertir la cuestión, En lugar de extraer si el dominio de Email para mirar si está en la lista que tenemos, lo suyo es pasarle la lista para que nos diga si su dominio está en ella:

 1 class Email
 2 {
 3     private string $username;
 4     private string $domain;
 5 
 6     private function __construct(string $username, string $domain)
 7     {
 8         $this->username = $username;
 9         $this->domain = $domain;
10     }
11 
12     public static function valid(string $email): self
13     {
14         if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
15             throw new InvalidArgumentException(sprintf('%s is not a val\
16 id email.', $email));
17         }
18 
19         [$username, $domain] = explode('@', $email);
20         return new self($username, $domain);
21     }
22 
23     public function belongsToOneOfThisDomains(array $domains): bool
24     {
25         return in_array($this->domain, $domains, true);
26     }
27 
28     public function __toString(): string
29     {
30         return $this->username . '@' . $this->domain;
31     }
32 }

Ahora podemos usarlo así:

1 class CanSeeBenefit
2 {
3     public function byCorporateEmail(Email $email)
4     {
5         return $email->belongsToOneOfThisDomains($this->getBenefitDomai\
6 ns->execute());
7     }
8 }

Con este cambio resulta que Email ya no tiene que mostrar ninguna de sus propiedades. Esto nos da libertad para cambiar su implementación sin tener que cambiar el código que lo usa. Y, por otro lado, expone comportamiento que puede ser usado por otros objetos interesados.

Quien se ha de encargar de construir un objeto

Este patrón se llama Creator y nos dice que la responsabilidad de crear un objeto debe recaer en aquel que tenga la información necesaria para hacerlo, o bien que coleccione o agrupe los objetos que se van a crear.

El ejemplo paradigmático de este patrón es el de un objeto que representa una factura y sus líneas. La responsabilidad de crear una línea de factura debería recaer en la propia factura, ya que aunque no tenga la información necesaria para crearla, sí que agrupa las líneas de factura. De hecho, las líneas de factura no tienen sentido fuera de una factura.

1 class Invoice
2 {
3     private array $lines = [];
4 
5     public function addLine(string $description, float $amount): void
6     {
7         $this->lines[] = new InvoiceLine($description, $amount);
8     }
9 }
 1 class InvoiceLine
 2 {
 3     private string $description;
 4     private float $amount;
 5 
 6     public function __construct(string $description, float $amount)
 7     {
 8         $this->description = $description;
 9         $this->amount = $amount;
10     }
11 }

Resumen del capítulo

En este capítulo introducimos algunos patrones útiles para decidir donde poner las responsabilidades en un sistema de software. Estos patrones son los GRASP, que nos ayudan a asignar responsabilidades a los objetos de un sistema. En concreto, hemos visto el patrón Information Expert y el patrón Creator.

Aplica Tell, Don’t Ask

En el que buscamos empujar el comportamiento dentro de nuestros objetos para que sepan hacer cosas por sí mismos, en lugar de preguntarles por lo que saben.

En la primera parte, hemos trabajado refactors muy orientados a mejorar la expresividad del código y a la organización de unidades de código. En esta segunda, estamos enfocándonos en la aplicación de varios principios de diseño orientado a objetos.

Los principios de diseño nos proporcionan criterios útiles tanto para guiarnos en el desarrollo como para evaluar código existente en el que tenemos que intervenir.

Notas de la segunda edición

En esta revisión, hemos dividido este capítulo en dos a fin de tratar de forma más detallada los principios presentados: Tell, Don’t Ask y la Ley de Demeter. Esto nos permitirá explorarlos con más detalle y mejores ejemplos.

Tell, don’t ask

La traducción de este enunciado a español sería algo así como “Pide, no preguntes”. La idea de fondo de este principio es que cuando queremos modificar un objeto basándose su propio estado, no es buena idea preguntarle por su estado (ask), hacer el cálculo y cambiar su estado si fuera preciso. En su lugar, lo propio sería encapsular ese proceso en un método del propio objeto y decirle (tell) que lo realice él mismo.

Dicho en otras palabras: cada objeto es responsable de su estado, representado por sus propiedades internas, y lo mantiene oculto a los demás objetos, que solo conocerán su interfaz pública. Este principio se conoce como Information hiding y es uno de los fundamentos de la orientación a objetos. Consecuentemente, las relaciones entre los objetos deben producirse siempre mediante llamadas a métodos, evitando acceder a las propiedades internas de otros objetos.

Como siempre, vamos a verlo con un ejemplo. Imagina una aplicación para calcular la pintura necesaria para dar color a diversas superficies. Un concepto importante es el área de las superficies a pintar, por lo que necesitamos poder representar esas superficies y calcular su área. Así que empezamos por una clase Square que representa un cuadrado:

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

Aquí tenemos una posible implementación de un AreaCalculator que da soporte a cuadrados.

1 class AreaCalculator
2 {
3     public function calculate($square)
4     {
5         $side = $square->side();
6         return $side**2;
7     }
8 }

Por supuesto, necesitamos poder representar más formas para combinarlas. Así que querremos calcular el área de otras figuras geométricas, como el triángulo o el círculo:

 1 class Triangle
 2 {
 3     private $base;
 4     private $height;
 5 
 6     public function __construct($base, $height)
 7     {
 8         $this->base = $base;
 9         $this->height = $height;
10     }
11 
12     public function base()
13     {
14         return $this->base;
15     }
16 
17     public function height()
18     {
19         return $this->height;
20     }
21 }

¿Cómo le explicamos a AreaCalculator que ahora tiene que calcular el área de un triángulo? Podríamos fijarnos en su tipo, para decidir qué algoritmo se debe aplicar:

 1 class AreaCalculator
 2 {
 3     public function calculate($shape)
 4     {
 5         if ($shape instanceof Square) {
 6             $side = $shape->side();
 7             return $side**2;
 8         }
 9 
10         if ($shape instanceof Triangle) {
11             $base = $shape->base();
12             $height = $shape->height();
13             return ($base * $height) / 2;
14         }
15     }
16 }

Como se puede ver en el código, AreaCalculator tiene que saber un montón de cosas acerca de los objetos de los que tiene que calcular su área:

  • Necesita saber qué tipo de objeto es (Square o Triangle).
  • Necesita saber qué propiedades tiene cada objeto, según el tipo de objeto (side o base y height).
  • Necesita saber cómo calcular el área de cada objeto.

Esto viola todos los principios relacionados con la encapsulación. AreaCalculator tiene que conocer demasiados detalles de los objetos que tiene que calcular. Además, si añadimos una nueva figura geométrica, tendremos que modificar AreaCalculator para añadir una nueva decisión:

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

Y una nueva modificación en AreaCalculator:

 1 class AreaCalculator
 2 {
 3     public function calculate($shape)
 4     {
 5         if ($shape instanceof Square) {
 6             $side = $shape->side();
 7             return $side**2;
 8         }
 9 
10         if ($shape instanceof Triangle) {
11             $base = $shape->base();
12             $height = $shape->height();
13             return ($base * $height) / 2;
14         }
15 
16         if ($shape instanceof Circle) {
17             $radius = $shape->radius();
18             return pi() * $radius**2;
19         }
20     }
21 }

Este código muestra un smell conocido como Data Class: las clases que representan las distintas figuras geométricas solo guardan sus datos, pero no tienen comportamiento. Eso hace que AreaCalculator tenga que aportar ese comportamiento y conocer demasiados detalles de los objetos. Además, si añadimos una nueva figura geométrica, tendremos que modificar AreaCalculator para añadir una nueva decisión. La solución a este problema es aplicar refactorings que nos permitan aplicar el principio Tell, Don’t Ask. Por ejemplo, moviendo el cálculo del área a cada figura geométrica. A este refactoring se le llama Move Method.

Aquí tenemos el cuadrado:

 1 class Square
 2 {
 3     private $side;
 4 
 5     public function __construct($side)
 6     {
 7         $this->side = $side;
 8     }
 9 
10     public function side()
11     {
12         return $this->side;
13     }
14 
15     public function area()
16     {
17         return $this->side**2;
18     }
19 }

El triángulo:

 1 class Triangle
 2 {
 3     private $base;
 4     private $height;
 5 
 6     public function __construct($base, $height)
 7     {
 8         $this->base = $base;
 9         $this->height = $height;
10     }
11 
12     public function base()
13     {
14         return $this->base;
15     }
16 
17     public function height()
18     {
19         return $this->height;
20     }
21 
22     public function area()
23     {
24         return ($this->base * $this->height) / 2;
25     }
26 }

Y el círculo:

 1 class Circle
 2 {
 3     private $radius;
 4 
 5     public function __construct($radius)
 6     {
 7         $this->radius = $radius;
 8     }
 9 
10     public function radius()
11     {
12         return $this->radius;
13     }
14 
15     public function area()
16     {
17         return pi() * $this->radius**2;
18     }
19 }

Puesto que ahora cada figura sabe cómo calcular su área, AreaCalculator se simplifica. Vamos a hacerlo por pasos y así entender mejor cómo cambian las cosas cuando dejamos de preocuparnos por las propiedades de los objetos y nos centramos en lo que les podemos pedir que hagan por nosotras. En el primer paso, nos limitamos a reemplazar el cálculo del área por el método area de cada figura. Así quedan tras la introducción del principio Tell, Don’t Ask a nuestro problema.

 1 class AreaCalculator
 2 {
 3     public function calculate($shape)
 4     {
 5         if ($shape instanceof Square) {
 6             return $shape->area();
 7         }
 8         
 9         if ($shape instanceof Triangle) {
10             return $shape->area();
11         }
12         
13         if ($shape instanceof Circle) {
14             return $shape->area();
15         }
16     }
17 }

Esto nos llevará a descubrir otro patrón de diseño que nos ayudará a simplificar aún más nuestro código: el polimorfismo. Como podemos ver, tenemos varios objetos que responden a los mismos mensajes, aunque sean de tipos distintos. Usando Polimorfismo dejaremos de preocuparnos por el tipo de objeto, y nos centraremos en el mensaje que queremos enviar. Tanto es así, que AreaCalculator puede simplificarse hasta llegar a lo siguiente:

1 class AreaCalculator
2 {
3     public function calculate($shape)
4     {
5          return $shape->area();
6     }
7 }

Pero no nos adelantemos. Dentro de un par de capítulos hablaremos de como aplicar polimorfismo. Antes, nos ocuparemos de ciertas reglas de convivencia que los objetos deben seguir para que todo funcione de la mejor forma posible.

Resumen del capítulo

El principio Tell, Don’t Ask nos ayuda a mejorar la encapsulación de nuestros objetos. En lugar de preguntarles por su estado y calcular el resultado en otro lugar, les pedimos que realicen la operación ellos mismos. De esta forma, cada objeto es responsable de su estado y de las operaciones que se pueden realizar con él.

Por otro lado, la aplicación del principio, puede llevarnos a describir oportunidades para aplicar otros patrones beneficios. Uno de ellos, es el polimorfismo, que nos permite enviar el mismo mensaje a diferentes objetos, para que cada uno de ellos lo interprete de la forma que le corresponda. Gracias al polimorfismo el código se simplifica al organizarlo en objetos pequeños y muy especializados, que son fáciles de entender y de testear. Por otro lado, eso nos permite llevar las decisiones a las factorías, que se encargan de seleccionar el objeto adecuado para cada situación.

Aplica la Ley de Demeter

En el que seguimos hablando acerca de la redistribución de responsabilidades. Un sistema orientado a objetos se basa en los mensajes que se envían los objetos entre sí, por lo que resulta importante aprender qué objetos pueden hablar entre sí y cuáles no.

Ley de Demeter

La Ley de Demeter1, que también se conoce como Principio de mínimo conocimiento, dice que un objeto no debería conocer la organización interna de los otros objetos con los que colabora. Lo único que debería saber es cómo comunicarse con ellos. De este modo, al conocer lo mínimo posible, se acopla mínimamente a ellos.

Notas de la segunda edición

Como mencionamos en el anterior, hemos separado los capítulos de Tell, Don’t Ask y Ley de Demeter en dos capítulos distintos. Aunque ambos tratan sobre la redistribución de responsabilidades, el anterior se centra en la encapsulación de la lógica de negocio, mientras que este se centra en la comunicación entre objetos.

Cumpliendo la Ley

Siguiendo la Ley de Demeter, como veremos, un método de una clase solo puede hablar con los objetos a los que conoce. Estos son:

  • La propia clase, de la que puede usar todos sus métodos.
  • Objetos que son propiedades de esa clase.
  • Objetos creados en el mismo método que los usa.
  • Objetos pasados como parámetros a ese método.

La finalidad de la ley de Demeter es evitar el acoplamiento estrecho entre objetos. Si un método usa un objeto, contenido en otro objeto que ha recibido o creado, implica un conocimiento que va más allá de la interfaz pública del objeto intermedio.

Vamos a ver esto con un ejemplo un poco amañado, pero que nos permitirá ilustrar varios problemas, empezando por algunos más superficiales y llegando a otros relacionados con el diseño de la solución.

Aquí tenemos una clase Product que representa un producto en una tienda online. Cada producto tiene un precio unitario y una promoción que se aplica si se compran más unidades de una cantidad determinada.

 1 class Product
 2 {
 3     private float $unitPrice;
 4     private Promotion $promotion;
 5 
 6     public function __construct(float $unitPrice, Promotion $promotion)
 7     {
 8         $this->unitPrice = $unitPrice;
 9         $this->promotion = $promotion;
10     }
11 
12     public function unitPrice(): float
13     {
14         return $this->unitPrice;
15     }
16 
17     public function currentPromotion(): Promotion
18     {
19         return $this->promotion;
20     }
21 }

Aquí tenemos Promotion, que contiene la información sobre el descuento aplicable.

 1 class Promotion
 2 {
 3     private int $threshold;
 4     private float $discountRate;
 5 
 6     public function __construct(int $threshold, float $discountRate)
 7     {
 8         $this->threshold = $threshold;
 9         $this->discountRate = $discountRate;
10     }
11 
12     public function threshold(): int
13     {
14         return $this->threshold;
15     }
16 
17     public function discountRate(): float
18     {
19         return $this->discountRate;
20     }
21 }

Ambas clases ya nos muestran un problema de padecer el smell Data Class o, dicho en otras palabras, de ser clases anémicas y no tener comportamiento propio. No obstante, vamos a seguir adelante con el ejemplo a ver a dónde nos lleva.

Esta es la calculadora de precios. Aquí podemos ver que necesitamos obtener Promotion con el fin de calcular los descuentos aplicables y el límite mínimo de unidades.

 1 class PriceCalculator
 2 {
 3     public function calculatePrice(Product $product, int $units): float
 4     {
 5         $promotion = $product->currentPromotion();
 6         if ($units > $promotion->threshold()) {
 7             return $product->unitPrice() * $units * (1 - $promotion->di\
 8 scountPct());
 9         }
10         return $product->unitPrice() * $units;
11     }
12 }

En el ejemplo, el método calculatePrice obtiene el descuento aplicable llamando a un método de Product, que devuelve otro objeto al cual le preguntamos sobre los datos necesarios para realizar el descuento. Aquí tenemos un caso de Inappropriate Intimacy, porque PriceCalculator está hablando con un objeto que no debería conocer directamente, ya que está dentro de Product.

La violación de la Ley de Demeter se produce aquí porque el método calculatePrice está hablando con un objeto que no conoce directamente, sino a través de otro. ¿Qué objeto es este y cuál es su interfaz? Podemos suponer que se trata de un objeto Promotion, pero eso es algo que sabemos nosotros, no el código. Este es el exceso de conocimiento que la Ley de Demeter trata de evitar.

¿Cómo podemos refactorizar este tipo de problemas y cumplir con la Ley de Demeter? Antes de nada, hay que advertir que no existe una solución única. Esta dependerá del contexto y de la correcta atribución de responsabilidades a los distintos objetos. Así que vamos a analizar varias opciones:

Pasa el objeto intermedio a otro método del objeto consumidor

Aplicando la letra de la Ley de Demeter, un método que recibe el objeto intermedio como parámetro no viola la ley. Lo ideal sería que fuera el único parámetro del método, cosa que no siempre es fácil de conseguir. Por ejemplo, podríamos hacer algo así, devolviendo el porcentaje de descuento, o cero si no se puede aplicar:

 1 class PriceCalculator
 2 {
 3     public function calculatePrice(Product $product, int $units): float
 4     {
 5         $discountPct = $this->discountPct($product->currentPromotion(),\
 6  $units);
 7         
 8         return $product->unitPrice() * $units * (1 - $discountPct);
 9     }
10 
11     private function discountPct(Promotion $promotion): float
12     {
13         if ($units > $promotion->threshold()) {
14             return $promotion->discountRate();
15         }
16         return 0;
17     }
18 }

No se trata de la mejor solución posible, pero es un paso que nos puede ayudar a entender mejor lo que necesitamos del objeto extraño y así poder reorganizar las responsabilidades. Así, por ejemplo, nos ayuda a ver que una posible solución sería mover esa lógica a Product.

Encapsular el comportamiento en el objeto que conocemos (Tell, Don’t Ask)

En algunos casos, en lugar de pedirle a nuestro objeto conocido que nos dé el otro objeto, podemos pedirle que haga lo que sea necesario con él y nos devuelva el resultado. En este caso, podríamos pedirle a Product que nos devuelva el precio total para un número de unidades. De esta forma, ni siquiera necesitaríamos saber si hay una promoción o no.

 1 class Product
 2 {
 3     public function totalPrice(int $units): float
 4     {
 5         $promotion = $this->currentPromotion();
 6         if ($units > $promotion->threshold()) {
 7             return $this->unitPrice() * $units * (1 - $promotion->disco\
 8 untRate());
 9         }
10         return $this->unitPrice() * $units;
11     }
12 }
13 
14 class PriceCalculator
15 {
16     public function calculatePrice(Product $product, int $units): float
17     {
18         return $product->totalPrice($units);
19     }
20 }

Esta es una solución interesante para muchos casos. Si tenemos modelos anémicos, tendemos a usar sus propiedades de forma directa, o a través de getters, para hacer cálculos. En lugar de eso, podemos pedirle al objeto que haga el cálculo por nosotros, ya que contiene toda la información necesaria y, por tanto, debería ser su responsabilidad. Es, ni más ni menos, que el Information Expert.

Esto es una clara mejora con respecto a la situación inicial, pero seguramente podemos hacerlo mejor. Hay un par de cosas que nos chirrían:

  • ¿Por qué no aplicar el mismo principio Tell, Don’t Ask a Promotion? Estamos preguntando por el descuento y el umbral, cuando podríamos pedirle a Promotion que nos devuelva el descuento aplicable para el número de unidades.
  • ¿Tiene sentido que Product sepa cómo calcular el precio total? Es más, ¿tiene sentido que Product sepa si hay una promoción o no? ¿No sería mejor que otro objeto se encargara de eso?

Moviendo responsabilidades: otra vez Tell, Don’t Ask

Retomemos el primer intento de refactor. En ese caso, pasábamos el objeto intermedio como parámetro a otro método, siguiendo la letra de la ley de Demeter. Igualmente, podemos ver que estamos preguntando a Promotion por el descuento y el umbral, cuando podríamos pedirle que nos devuelva el descuento aplicable para un número de unidades.

 1 class PriceCalculator
 2 {
 3     public function calculatePrice(Product $product, int $units): float
 4     {
 5         $discountPct = $this->discountPct($product->currentPromotion(),\
 6  $units);
 7         
 8         return $product->unitPrice() * $units * (1 - $discountPct);
 9     }
10 
11     private function discountPct(Promotion $promotion, int $units): flo\
12 at
13     {
14         if ($units > $promotion->threshold()) {
15             return $promotion->discountRate();
16         }
17         return 0;
18     }
19 }

Es decir, dado que Promotion es el que sabe cómo calcular el descuento aplicable, es lógico que sea él quien lo haga. Y lo único que tenemos que hacer es encapsular esa lógica en un método de Promotion. De nuevo, un caso de Information Expert.

 1 class Promotion
 2 {
 3     public function discountPct(int $units): float
 4     {
 5         if ($units > $this->threshold()) {
 6             return $this->discountRate();
 7         }
 8         return 0;
 9     }
10 }
11 
12 class PriceCalculator
13 {
14     public function calculatePrice(Product $product, int $units): float
15     {
16         $discountPct = $this->discountPct($product->currentPromotion(),\
17  $units);
18         
19         return $product->unitPrice() * $units * (1 - $discountPct);
20     }
21 
22     private function discountPct(Promotion $promotion, int $units): flo\
23 at
24     {
25         return $promotion->discountPct($units);
26     }
27 }

Pero, como hemos dicho, ya que Promotion está contenido en Product, lo suyo sería que pedirle a Product que se encargue de gestionar esos cálculos. De esta forma, Product sería el que conoce la estructura de precios y cómo aplicar los descuentos y no tendríamos que saber nada de Promotion fuera de Product.

 1 class Promotion
 2 {
 3     public function discountPct(int $units): float
 4     {
 5         if ($units > $this->threshold()) {
 6             return $this->discountRate();
 7         }
 8         return 0;
 9     }
10 }
11 
12 class Product
13 {
14     public function totalPrice(int $units): float
15     {
16         $promotion = $this->currentPromotion();
17         return $this->unitPrice() * $units * (1 - $promotion->discountP\
18 ct());
19     }
20 }
21 
22 class PriceCalculator
23 {
24     public function calculatePrice(Product $product, int $units): float
25     {
26         return $product->totalPrice($units);
27     }
28 }

Como nos preguntábamos antes, nos molesta un poco que Product sepa tanto sobre precios y promociones. Promotion podría ser mejor lugar para tener la lógica de descuentos. ¿Y si Promotion fuese la encargada de calcular el precio total?

 1 class Promotion
 2 {
 3     public function totalPrice(float $unitPrice, int $units): float
 4     {
 5         if ($units > $this->threshold()) {
 6             return $unitPrice * $units * (1 - $this->discountRate());
 7         }
 8         
 9         return $unitPrice * $units;
10     }
11 }
12 
13 class Product
14 {
15     public function totalPrice(int $units): float
16     {
17         $promotion = $this->currentPromotion();
18         
19         return $promotion->totalPrice($this->unitPrice(), $units);
20     }
21 }
22 
23 class PriceCalculator
24 {
25     public function calculatePrice(Product $product, int $units): float
26     {
27         return $product->totalPrice($units);
28     }
29 }

Esta solución es mucho mejor porque las responsabilidades están mejor repartidas:

  • PriceCalculator solo coordina la llamada a Product con el número de unidades de las que vamos a calcular el importe total.
  • Product solo tiene que preocuparse de su precio unitario y de aplicar la promoción.
  • Promotion encapsula la estrategia de descuentos y hace los cálculos a partir de la información bruta de Product y el número de unidades.

Reasignación de responsabilidades

Si lo piensas cuidadosamente, esta nueva distribución de responsabilidades tiene mucho sentido. Promotion está encapsulando una estrategia de descuentos, mientras que Product solo tiene que preocuparse de su precio unitario y de aplicar la promoción. Esto parece vacíar de contenido a PriceCalculator, por lo que podríamos eliminarlo. Sin embargo, lo habitual sería que una tienda on-line pueda tener varios tipos de promociones y estrategias de precios. Sigue teniendo sentido que haya un objeto que se encargue de calcular el precio total de un producto, pero lo que no está bien es que una promoción está tan estrechamente ligada a un producto. Querremos poder tener la flexibilidad de gestionar los productos, por un lado, y las promociones y estrategias de precios, por otro.

Esto nos debería sonar a un patrón Strategy para decirle a PriceCalculator qué estrategia de precios aplicar al producto que le pasamos. Por tanto, en lugar de que Price contenga una referencia a Promotion, lo que hacemos es separarlos e inyectar Promotion cuando solo cuando se necesita:

 1 class Promotion
 2 {
 3     private float $discountRate;
 4     private int $threshold;
 5     
 6     public function totalPrice(float $unitPrice, int $units): float
 7     {
 8         if ($units > $this->threshold) {
 9             return $unitPrice * $units * (1 - $this->discountRate);
10         }
11         return $unitPrice * $units;
12     }
13 }
14 
15 class Product
16 {
17     private float $unitPrice;
18     
19     public function totalPrice(int $units, Promotion $promotion): float
20     {
21         return $promotion->totalPrice($this->unitPrice, $units);
22     }
23 }
24 
25 class PriceCalculator
26 {
27     public function calculatePrice(Product $product, int $units, Promot\
28 ion $promotion): float
29     {
30         return $product->totalPrice($units, $promotion);
31     }
32 }

Aprovecho para llamar tu atención sobre la clase Product. A fin de no exponer sus propiedades, sino de encapsularlas, lo que hacemos es pasarle el objeto Promotion y que sea él quien se encargue de calcular el precio total, para lo cual Product le pasa la información de su precio unitario. Esto es un ejemplo de un patrón Double Dispatch, en el que un objeto recibe otro para poder pasarle información. De este modo, no tenemos que exponer las propiedades de Product a Promotion, y tampoco al revés.

1 class Product
2 {
3     private float $unitPrice;
4     
5     public function totalPrice(int $units, Promotion $promotion): float
6     {
7         return $promotion->totalPrice($this->unitPrice, $units);
8     }
9 }

Ahora Product tiene el acoplamiento mínimo con Promotion y esta no se acopla a nada, mientras que PriceCalculator solo se encarga de coordinarlos.

Resumen del capítulo

La Ley de Demeter o Principio de Mínimo Conocimiento nos dice con qué otros objetos podemos hablar, que serán aquellos que conozcamos directamente:

  • El mismo objeto, del cual puede usar todos sus métodos.
  • Objetos que sean propiedades de ese objeto.
  • Objetos creados en el mismo método que los usa.
  • Objetos pasados como parámetros a ese método.

Aplicando este principio, podemos mejorar la organización de nuestro código distribuyendo las responsabilidades entre los objetos participantes y manteniendo el acoplamiento controlado.

Polimorfismo

Donde abordamos el problema de lidiar con variaciones de comportamiento de un objeto basadas en su tipo o, en general, de un aspecto de su estado.

Notas de la segunda edición

Este capítulo no estaba en la edición original, pero hemos reunido aquí elementos procedentes de otros capítulos.

Gestionar variantes de comportamiento basadas en tipo es fácil si sabes cómo

Recordemos el problema del nombre de una persona que usamos para trabajar la introducción de conceptos formados por varios componentes. En este caso, el nombre de una persona puede estar formado por un nombre y un 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 }

Un problema que te podrías encontrar es la diversidad de formatos de nombre que puedes encontrar en el mundo. Los nombres coreanos comienzan por el apellido. En USA es frecuente la inicial entre el nombre y el apellido. En España lo común es tener dos apellidos, mientras que en otros países solo se tiene uno. Una aplicación que pueda dar soporte a toda esta casuística debería tener un diseño que permita adaptarse a estos cambios.

¿Y qué ocurre si necesitamos representar el nombre coreano? Efectivamente, podemos crear un objeto KoreanName que se adapte a las necesidades de este caso:

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

Para que los objetos sean intercambiables entre sí, deberían declarar una interfaz común. Por ejemplo, PersonName e implementar los métodos necesarios para que puedan ser usados de forma intercambiable. En este caso, es __toString():

1 interface PersonName
2 {
3     public function __toString(): string;
4 }

Modificamos SimpleName y KoreanName para que implementen la interfaz:

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

Y de este modo, podemos introducir cualquier variante de nombre que necesitemos en nuestra aplicación.

Polimorfismo

Recordemos el calculador de superficies que imaginamos en un artículo anterior, que dejamos así después de aplicar el principio Tell, Don’t Ask:

 1 class AreaCalculator
 2 {
 3     public function calculate($shape)
 4     {
 5         if ($shape instanceof Square) {
 6             return $shape->area();
 7         }
 8         
 9         if ($shape instanceof Triangle) {
10             return $shape->area();
11         }
12         
13         if ($shape instanceof Circle) {
14             return $shape->area();
15         }
16     }
17 }

Sin embargo, seguimos preguntándole cosas a las figuras geométricas. En este caso, preguntamos si son instancias de Square, Triangle o Circle, que es básicamente preguntarles por una propiedad, y es obvio que la sucesión de if es redundante porque en último les estamos pidiendo que hagan lo mismo, aunque cada una lo resuelva de forma diferente. Dado que cada figura expone un método area, podemos asumir que todas ellas son capaces de responder al mismo mensaje. Por tanto, simplifiquemos AreaCalculator:

1 class AreaCalculator
2 {
3     public function calculate($shape)
4     {
5         return $shape->area();
6     }
7 }

Y aquí podemos ver el principio Tell, Don’t Ask en acción. En lugar de preguntar a las figuras geométricas por su estado y calcular el área en otro lugar, les pedimos que lo hagan ellas mismas independientemente de su tipo. Y lo que antes era un código confuso con montones de líneas, ahora se queda en una sola.

Esta solución es posible gracias al polimorfismo. El polimorfismo es una característica de los lenguajes orientados a objetos gracias a la cual podemos enviar el mismo mensaje a diferentes objetos, para que cada uno de ellos haga lo que le pedimos a su manera particular, que desconocemos.

El problema, en nuestro caso, es que al principio teníamos objetos anémicos a los que no podíamos enviar ningún mensaje. Una vez que hemos aplicado Tell, don’t ask y hemos movido el comportamiento a los objetos, nos hemos dado cuenta de que podríamos beneficiarnos del polimorfismo.

Por otro lado, nos hemos aprovechado de la posibilidad de PHP, y de otros lenguajes, de hacer “duck typing”, gracias a lo cual el método calculate no requiere tipado. En lugar de preguntar por el tipo de objeto, simplemente le pedimos que haga algo. Si el objeto sabe hacerlo, lo hará. Si no, lanzará una excepción. En algunos lenguajes de programación tendríamos que haber declarado el tipo de $shape en el método calculate para saber que se puede llamar a area.

Examinando las regularidades de las clases Square, Triangle y Circle, podemos ver que todas ellas tienen un método area que devuelve un valor numérico. Por tanto, podemos definir una interfaz común para todas ellas:

1 interface Shape
2 {
3     public function area(): float;
4 }

Gracias a esto, AreaCalculator puede confiar en que cualquier objeto que implemente la interfaz Shape tendrá un método area que devolverá un valor numérico. En caso de que el objeto recibido no implemente la interfaz Shape, PHP lanzará una excepción antes incluso de intentar ejecutar el método calculate.

1 class AreaCalculator
2 {
3     public function calculate(Shape $shape): float
4     {
5         return $shape->area();
6     }
7 }

Una ventaja extra, como se puede ver, es que ahora AreaCalculator no tiene que preocuparse por los detalles de cada figura geométrica. Solo necesita saber que el objeto que recibe implementa la interfaz Shape y que, por tanto, tiene un método area que puede llamar. Eso hace innecesario acceder directamente a las propiedades internas de cada figura.

 1 class Square implements Shape
 2 {
 3     private $side;
 4 
 5     public function __construct($side)
 6     {
 7         $this->side = $side;
 8     }
 9 
10     public function area(): float
11     {
12         return $this->side**2;
13     }
14 }
 1 class Triangle implements Shape
 2 {
 3     private $base;
 4     private $height;
 5 
 6     public function __construct($base, $height)
 7     {
 8         $this->base = $base;
 9         $this->height = $height;
10     }
11 
12     public function area(): float
13     {
14         return ($this->base * $this->height) / 2;
15     }
16 }
 1 class Circle implements Shape
 2 {
 3     private $radius;
 4 
 5     public function __construct($radius)
 6     {
 7         $this->radius = $radius;
 8     }
 9 
10     public function area(): float
11     {
12         return pi() * $this->radius**2;
13     }
14 }

Otros escondites del polimorfismo

El tipo muchas veces viene dado por la clase de los objetos, que es lo que ocurre en nuestro ejemplo, pero en otros casos es una propiedad de una única clase.

Imagina que, en lugar del AreaCalculator del ejemplo anterior, tuviésemos algo así. Expresamos el concepto de forma geométrica mediante una clase Shape que puede decirnos su superficie. Sin embargo, cada vez que se lo pedimos tiene que preguntarse “¿de qué tipo soy?”, para saber cómo calcularla. No solo, sino que además tiene que mantener datos internos que no le corresponden, como el lado de un cuadrado, la base y la altura de un triángulo o el radio de un círculo.

 1 class Shape
 2 {
 3     private $type;
 4     private $data;
 5 
 6     public function __construct($type, $data)
 7     {
 8         $this->type = $type;
 9         $this->data = $data;
10     }
11 
12     public function area(): float
13     {
14         switch ($this->type) {
15             case 'square':
16                 return $this->data['side'] ** 2;
17             case 'triangle':
18                 return ($this->data['base'] * $this->data['height']) / \
19 2;
20             case 'circle':
21                 return pi() * $this->data['radius'] ** 2;
22             default:
23                 throw new Exception("Invalid shape type");
24         }
25     }
26 }

Se trata de un caso similar al anterior, pero en lugar de preguntar por el tipo de objeto, que es siempre el mismo, preguntamos por una propiedad interna del objeto. En este caso, el polimorfismo se esconde en la propiedad type de la clase Shape. La solución es la misma: mover el comportamiento a objetos especializados y dejar que ellos se encarguen de calcular su área.

Para hacer este cambio de manera progresiva podríamos empezar por introducir un método factoría. A continuación podríamos reemplazar los usos de new Shape() con Shape::create().

 1 class Shape
 2 {
 3     private $type;
 4     private $data;
 5 
 6     private function __construct($type, $data)
 7     {
 8         $this->type = $type;
 9         $this->data = $data;
10     }
11 
12     public static function create($type, $data): Shape
13     {
14         return new self($type, $data);
15     }
16     
17      public function area(): float
18     {
19         switch ($this->type) {
20             case 'square':
21                 return $this->data['side'] ** 2;
22             case 'triangle':
23                 return ($this->data['base'] * $this->data['height']) / \
24 2;
25             case 'circle':
26                 return pi() * $this->data['radius'] ** 2;
27             default:
28                 throw new Exception("Invalid shape type");
29         }
30     }
31 }

A continuación, podríamos crear una clase para cada tipo de forma geométrica y mover el cálculo del área a cada una de ellas. Estas clases heredan extienden Shape y sobrescriben el método area. En este caso, podemos considerar que cada una de ellas es una especialización, pero podríamos hacerlo también con una interfaz común.

 1 class Square extends Shape
 2 {
 3     private $side;
 4 
 5     public function __construct($side)
 6     {
 7         $this->side = $side;
 8     }
 9 
10     public function area(): float
11     {
12         return $this->side**2;
13     }
14 }
 1 class Triangle extends Shape
 2 {
 3     private $base;
 4     private $height;
 5 
 6     public function __construct($base, $height)
 7     {
 8         $this->base = $base;
 9         $this->height = $height;
10     }
11 
12     public function area(): float
13     {
14         return ($this->base * $this->height) / 2;
15     }
16 }
 1 class Circle extends Shape
 2 {
 3     private $radius;
 4 
 5     public function __construct($radius)
 6     {
 7         $this->radius = $radius;
 8     }
 9 
10     public function area(): float
11     {
12         return pi() * $this->radius**2;
13     }
14 }

Ahora, en el constructor estático create de Shape, podemos decidir qué tipo de objeto devolver en función del tipo de forma geométrica que queremos crear.

 1 class Shape
 2 {
 3     public static function create($type, $data): Shape
 4     {
 5         switch ($type) {
 6             case 'square':
 7                 return new Square($data['side']);
 8             case 'triangle':
 9                 return new Triangle($data['base'], $data['height']);
10             case 'circle':
11                 return new Circle($data['radius']);
12             default:
13                 throw new Exception("Invalid shape type");
14         }
15     }
16     
17     public function area(): float
18     {
19         throw new Exception("Not implemented");
20     }
21 }

Con este cambio, el código que consume Shape, recibirá un objeto del tipo correcto, y no tendrá que preocuparse por la forma geométrica que está manejando. Solo tendrá que llamar al método area y el objeto se encargará de calcularla.

Resumen del capítulo

En este capítulo hemos visto de qué manera el polimorfismo nos permite manejar de forma sencilla las variantes de comportamiento relacionadas con el tipo de los objetos, centrando nuestra atención en los Roles que desempeñan, más que en el tipo de objeto de que se trate.

Esto lleva a mover las decisiones de comportamiento de los objetos fuera de los objetos y hacia las factorías. La factoría nos proporciona el objeto con el que debemos hablar y nos garantiza que el objeto que recibimos sabe cómo responder a los mensajes que le enviamos.

Gestionar estados y transformaciones

Capítulo en el que se descubre como gestionar estados y transformaciones de un objeto, a través de un objeto capaz de decidir qué transformaciones entre estados son válidas y cuáles no.

Notas de la segunda edición

Hemos extraído parte del contenido del capítulo dedicado a enumerables para introducir un patrón que puede ser muy útil en aplicaciones complejas. Se trata de la máquina de estados o State Pattern.

Representando estados

Vamos a empezar por un ejemplo muy sencillo. Es bastante típico tener que modelar que un cierto objeto puede estar en un estado Activo o Inactivo. Así, un Usuario puede enrolarse en nuestro sistema y estar inactivo hasta que verifica su email, lo que sirve como una forma de protección sencilla para evitar que se creen cuentas falsas. La siguiente es una aproximación a cómo podríamos modelar esto, bastante típica:

 1 class ActivationStatus
 2 {
 3     public const ACTIVE = 'active';
 4     public const INACTIVE = 'inactive';
 5     
 6     private $status;
 7     
 8     public function __construct(string $status)
 9     {
10         if (!in_array($status, [self::ACTIVE, self::INACTIVE])) {
11             throw new \InvalidArgumentException('Invalid status');
12         }
13         $this->status = $status;
14     }
15 }

Hay un par de cosas que nos interesa señalar aquí. En primer lugar, hemos definido dos constantes para representar los dos estados posibles. Esto es una buena práctica, ya que nos permite evitar errores tipográficos y nos facilita la vida a la hora de trabajar con estos valores. En segundo lugar, hemos definido un constructor que recibe un string y comprueba que el valor sea uno de los dos estados posibles. Si no lo es, lanza una excepción. Esto es importante, porque nos asegura que el objeto siempre estará en un estado válido.

El siguiente paso en pensar en las transformaciones que puede sufrir un objeto. En este caso, un usuario puede pasar de inactivo a activo, pero no al revés. Obviamente, si intentamos activar un usuario que ya está activo, no debería suceder nada.

Las transformaciones posibles se representan mediante métodos, así que podríamos añadir activate y deactivate.

 1 class ActivationStatus
 2 {
 3     public const ACTIVE = 'active';
 4     public const INACTIVE = 'inactive';
 5     
 6     private $status;
 7     
 8     public function __construct(string $status)
 9     {
10         if (!in_array($status, [self::ACTIVE, self::INACTIVE])) {
11             throw new \InvalidArgumentException('Invalid status');
12         }
13         $this->status = $status;
14     }
15     
16     public function activate(): void
17     {
18         $this->status = self::ACTIVE;
19     }
20     
21     public function deactivate(): void
22     {
23         if ($this->status === self::ACTIVE) {
24             throw new \DomainException('Can not deactivate an active us\
25 er');
26         }
27         $this->status = self::INACTIVE;
28     }
29 }

Un primer problema es que estamos mutando el objeto ActuationStatus. Esto no es necesariamente malo, pero puede llevar a errores si no tenemos cuidado. El objeto que muta, en nuestro ejemplo sería User, y ActivationStatus está mejor representado como un Value Object, ya que es un concepto que nos interesa por valor. Los Value Objects queremos que sean inmutables:

 1 class ActivationStatus
 2 {
 3     public const ACTIVE = 'active';
 4     public const INACTIVE = 'inactive';
 5     
 6     private $status;
 7     
 8     public function __construct(string $status)
 9     {
10         if (!in_array($status, [self::ACTIVE, self::INACTIVE])) {
11             throw new \InvalidArgumentException('Invalid status');
12         }
13         $this->status = $status;
14     }
15     
16     public function activate(): self
17     {
18         return new self(self::ACTIVE);
19     }
20     
21     public function deactivate(): self
22     {
23         if ($this->status === self::ACTIVE) {
24             throw new \DomainException('Can not deactivate an active us\
25 er');
26         }
27         return new self(self::INACTIVE);
28     }
29 }

El objeto User, por otro lado, tendrá métodos para cambiar su estado:

 1 class User
 2 {
 3     private $activationStatus;
 4     
 5     private function __construct(ActivationStatus $activationStatus)
 6     {
 7         $this->activationStatus = $activationStatus;
 8     }
 9     
10     public static function enroll(): self
11     {
12         return new self(new ActivationStatus(ActivationStatus::INACTIVE\
13 ));
14     }
15     
16     
17     public function activate(): void
18     {
19         $this->activationStatus = $this->activationStatus->activate();
20     }
21     
22     public function deactivate(): void
23     {
24         $this->activationStatus = $this->activationStatus->deactivate();
25     }
26 }

Como puedes ver, el método enroll nos permite crear un usuario que comienza su andadura en el sistema estando inactivo. Los métodos activate y deactivate nos permiten cambiar el estado del usuario cuando sea necesario. La lógica de esos métodos está en el objeto ActivationStatus, que es el que realmente sabe cómo cambiar de estado.

Mejorando lo presente

Hay un par de cosas que resultan molestas en ActivationStatus. Un solo objeto debe conocer toda la lógica de activación y desactivación. Para dos estados no es mucho problema, pero seguramente nos encontraremos con casos con muchos más estados y con lógica más compleja. Así que vamos a ver cómo podemos mejorar esto.

En primer lugar, vamos a representar cada estado con una clase distinta, aunque ambas serán igualmente ActivationStatus. Este es un uso bastante justificable de la herencia, ya que ambas clases son del mismo tipo y comparte interfaz.

 1 class ActiveActivationStatus extends ActivationStatus
 2 {
 3     public function activate(): self
 4     {
 5         return $this;
 6     }
 7     
 8     public function deactivate(): self
 9     {
10         throw new \DomainException('Can not deactivate an active user');
11     }
12 }
13 
14 class InactiveActivationStatus extends ActivationStatus
15 {
16     public function activate(): self
17     {
18         return new ActiveActivationStatus();
19     }
20     
21     public function deactivate(): self
22     {
23         return $this;
24     }
25 }

La clase ActivationStatus ahora será abstracta y tendrá dos métodos abstractos, activate y deactivate. Las clases ActiveActivationStatus e InactiveActivationStatus implementarán estos métodos de forma distinta. La clase ActivationStatus será la que se encargue de crear las instancias de las clases concretas, por lo que se convierte en una factoría:

 1 abstract class ActivationStatus
 2 {
 3     public static function inactive(): self
 4     {
 5         return new InactiveActivationStatus();
 6     }
 7     
 8     public static function active(): self
 9     {
10         return new ActiveActivationStatus();
11     }
12 }

De esta forma, la clase User puede hacer uso de la misma:

 1 class User
 2 {
 3     private $activationStatus;
 4     
 5     private function __construct(ActivationStatus $activationStatus)
 6     {
 7         $this->activationStatus = $activationStatus;
 8     }
 9     
10     public static function enroll(): self
11     {
12         return new self(ActivationStatus::inactive());
13     }
14     
15     public function activate(): void
16     {
17         $this->activationStatus = $this->activationStatus->activate();
18     }
19     
20     public function deactivate(): void
21     {
22         $this->activationStatus = $this->activationStatus->deactivate();
23     }
24 }

Ahora bien, puede que te estés haciendo algunas preguntas:

¿Cómo podemos obtener el valor del estado actual, por ejemplo, para la persistencia o la serialización? Pues es bastante sencillo, tan solo necesitamos un método en cada subtipo de ActivationStatus que nos devuelva un valor para representar el estado actual serializado. Podríamos simplemente implementar un __toString().

 1 class ActiveActivationStatus extends ActivationStatus
 2 {
 3     public function activate(): self
 4     {
 5         return $this;
 6     }
 7     
 8     public function deactivate(): self
 9     {
10         throw new \DomainException('Can not deactivate an active user');
11     }
12     
13     public function __toString(): string
14     {
15         return ActivationStatus::ACTIVE;
16     }
17 }

¿Cómo podemos reconstruir el estado a partir de un valor serializado? Pues también es sencillo, podemos añadir un método estático a ActivationStatus que nos permita hacer esto:

 1 abstract class ActivationStatus
 2 {
 3     public const ACTIVE = 'active';
 4     public const INACTIVE = 'inactive';
 5     
 6     public static function fromString(string $status): self
 7     {
 8         switch ($status) {
 9             case self::ACTIVE:
10                 return new ActiveActivationStatus();
11             case self::INACTIVE:
12                 return new InactiveActivationStatus();
13             default:
14                 throw new \InvalidArgumentException('Unsupported status\
15 ');
16         }
17     }
18 }

¿Qué comportamientos deberían implementar los métodos activate y deactivate en la clase abstracta? Esto va a depender de tus preferencias o de tus necesidades. En nuestro caso, hemos decidido que activate y deactivate devuelvan una nueva instancia del objeto, pero podrían lanzar una excepción, o simplemente ser declarados como abstractos, lo que te obliga a implementarlos en cualquier subclase.

Pues con esto, ya tenemos un ejemplo de un patrón State muy simple, de dos estados con dos transformaciones posibles.

Máquinas más complejas

La complejidad de una máquina de estados es una función de los estados que puede mantener y de las transformaciones que puede realizar. En el ejemplo anterior, teníamos dos estados y dos transformaciones. En un caso más complejo, podríamos tener muchos más estados y muchas más transformaciones. Por ejemplo, podríamos tener un objeto que represente un pedido, que puede estar en estado nuevo, pagado, enviado o entregado. Las transformaciones posibles podrían ser pagar, enviar y entregar. En este caso, la máquina de estados sería más compleja, pero el patrón seguiría siendo el mismo.

 1 abstract class OrderStatus
 2 {
 3     public static function new(): self
 4     {
 5         return new NewOrderStatus();
 6     }
 7     
 8     public static function paid(): self
 9     {
10         return new PaidOrderStatus();
11     }
12     
13     public static function sent(): self
14     {
15         return new SentOrderStatus();
16     }
17     
18     public static function delivered(): self
19     {
20         return new DeliveredOrderStatus();
21     }
22     
23     abstract public function pay(): self;
24     
25     abstract public function send(): self;
26     
27     abstract public function deliver(): self;
28 }

En este ejemplo, las transformaciones nos llevan a un nuevo estado, pero no todas ellas están permitidas. Por ejemplo, un pedido nuevo no puede ser entregado si no ha sido pagado ni enviado. O no se puede enviar un pedido que no haya sido pagado. Estos son ejemplos de transformaciones que se pueden validar con la información del propio objeto. Esto es: cada objeto representando un estado sabe por sí mismo qué transformaciones puede hacer y cuáles no.

Pero en muchos casos de uso, las transformaciones de estado dependen de más cuestiones. Por ejemplo, un pedido no se puede enviar si no se ha pagado, pero tampoco se puede enviar si no hay stock, o si no tenemos una dirección de envío, o no se puede pagar si el pedido no contiene productos. En estos casos, el objeto representando el estado no tiene toda la información que necesita y debe recibir algún tipo de contexto para tomar la decisión, que puede ser distinto para cada transformación.

 1 abstract class OrderStatus
 2 {
 3     public static function new(): self
 4     {
 5         return new NewOrderStatus();
 6     }
 7     
 8     public static function paid(): self
 9     {
10         return new PaidOrderStatus();
11     }
12     
13     public static function sent(): self
14     {
15         return new SentOrderStatus();
16     }
17     
18     public static function delivered(): self
19     {
20         return new DeliveredOrderStatus();
21     }
22     
23     abstract public function pay(Order $order): self;
24     
25     abstract public function send(Order $order): self;
26     
27     abstract public function deliver(Order $order): self;
28 }

La forma concreta de pasar ese contexto puede ser diferente según los casos. Para nuestro ejemplo, vamos a suponer que el objeto Order expone métodos que nos dicen si los productos están en stock, si tiene una dirección de envío, detalles de pago, etc. En otros casos, el contexto podría ser un objeto que represente esas condiciones que cada estado debe tener en cuenta para decidir si la transformación es válida.

 1 class Order
 2 {
 3     private $status;
 4     
 5     private function __construct(ActivationStatus $status)
 6     {
 7         $this->status = $status;
 8     }
 9     
10     public static function new(): self
11     {
12         return new self(ActivationStatus::new());
13     }
14     
15     public function pay(): void
16     {
17         $this->status = $this->status->pay($this);
18     }
19     
20     public function send(): void
21     {
22         $this->status = $this->status->send($this);
23     }
24     
25     public function deliver(): void
26     {
27         $this->status = $this->status->deliver($this);
28     }
29     
30     public function isValid(): bool
31     {
32         return !$this->products->empty() ;
33     }
34     
35     public function hasShippingAddress(): bool
36     {
37         return $this->shippingAddress->isValid();
38     }
39     
40     public function hasPaymentDetails(): bool
41     {
42         return $this->paymentDetails->isValid();
43     }
44 }

Así que veamos cómo sería cada estado concreto:

 1 class NewOrderStatus extends OrderStatus
 2 {
 3     public function pay(Order $order): self
 4     {
 5         if ($order->hasPaymentDetails()) {
 6             return OrderStatus::paid();
 7         }
 8         throw new \DomainException('Can not pay an order without paymen\
 9 t details');
10     }
11     
12     public function send(Order $order): self
13     {
14         throw new \DomainException('Can not send an unpaid order');
15     }
16     
17     public function deliver(Order $order): self
18     {
19         throw new \DomainException('Can not deliver an unpaid order');
20     }
21 }

El pedido ya pagado no se puede pagar de nuevo, pero sí enviar, siempre que tenga una dirección de envío.

 1 class PaidOrderStatus extends OrderStatus
 2 {
 3     public function pay(Order $order): self
 4     {
 5         throw new \DomainException('Can not pay an already paid order');
 6     }
 7     
 8     public function send(Order $order): self
 9     {
10         if ($order->hasShippingAddress()) {
11             return OrderStatus::sent();
12         }
13         throw new \DomainException('Can not send an order without a shi\
14 pping address');
15     }
16     
17     public function deliver(Order $order): self
18     {
19         throw new \DomainException('Can not deliver an order that has n\
20 ot been sent');
21     }
22 }

Cuando un pedido se ha enviado, ya no se puede enviar de nuevo, ni pagar, pero sí entregar.

 1 class SentOrderStatus extends OrderStatus
 2 {
 3     public function pay(Order $order): self
 4     {
 5         throw new \DomainException('Can not pay an order that has been \
 6 sent');
 7     }
 8     
 9     public function send(Order $order): self
10     {
11         throw new \DomainException('Can not send an order that has been\
12  sent');
13     }
14     
15     public function deliver(Order $order): self
16     {
17         return OrderStatus::delivered();
18     }
19 }

Y, finalmente, DeliveredOrderStatus, que es quien que finaliza el proceso, garantizando que no es posible volver a un estado anterior.

 1 class DeliveredOrderStatus extends OrderStatus
 2 {
 3     public function pay(Order $order): self
 4     {
 5         throw new \DomainException('Can not pay an order that has been \
 6 delivered');
 7     }
 8     
 9     public function send(Order $order): self
10     {
11         throw new \DomainException('Can not send an order that has been\
12  delivered');
13     }
14     
15     public function deliver(Order $order): self
16     {
17         throw new \DomainException('Can not deliver an order that has b\
18 een delivered');
19     }
20 }

Por supuesto, nos han quedado algunos estados en el tintero, como CancelledOrderStatus, que podría ser útil para representar un pedido que ha sido cancelado. Incluso podría aplicarse ese estado en lugar de lanzar excepciones cuando no se dispone de medio de pago o de dirección de envío. Otro estado interesante sería ReturnedOrderStatus, representando un pedido devuelto. Pero con esto, ya tienes una idea de cómo puedes implementar una máquina de estados más compleja y te propongo realizar el ejercicio de proseguir con la implementación de estos estados. Imagina, también donde pondrías un estado ValidatedOrderStatus que represente un estado en el que el pedido ha sido validado en todos los detalles necesarios.

Resumen del capítulo

El patrón State nos permite modelar y tener bajo control los estados y posibles transformaciones de un objeto a lo largo de su ciclo de vida en un sistema.

Anexos

  • Lecturas recomendadas y otros recursos

Lecturas recomendadas

Donde se exponen algunos libros, artículos y vídeos que pueden ser un buen recurso para aprender de autores más cualificados y prestigiosos que el de este libro, cosa que tampoco es tan difícil de encontrar.

Libros recomendados

Refactoring, de Martin Fowler y Kent Beck. Se trata de un libro de referencia. Es difícil de leer de un tirón porque buena parte de su contenido es un catálogo de técnicas, pero el capítulo sobre code smells es posiblemente el más iluminador de todos. Existen dos ediciones de este libro. En la primera, el refactoring era un concepto todavía en exploración y hay algunos apéndices en los que se trata esa cuestión. En la segunda edición, los ejemplos están en Javascript, lo que posiblemente los hace más accesibles.

Smells to Refactorings es un artículo de Joshua Kerievsky que, como su nombre indica, trata de los code smells y cómo refactorizarlos. Es un buen complemento al libro de Fowler y Beck.

Tidy First?: A Personal Exercise in Empirical Software Design de Kent Beck. Este libro es pequeño y se lee en un suspiro, pero te puede dejar pensando por meses. En él, Kent Beck explica formas de abordar el refactoring en un proyecto, a través de pequeños pasos inocuos que nos permiten ir mejorando el código sin miedo a romperlo. La última parte del libro la dedica a explicar el valor económico de la práctica de refactoring. En algunos sentidos, es un libro similar a este que tienes entre manos, pero lo ha escrito Kent Beck, así que es mucho mejor.

Five Lines of Code, de Christian Clausen. Soy consciente de que el prólogo por el inefable Robert C. Martin puede echar para atrás a más de una, pero el libro de Clausen merece una lectura. El autor propone una metodología de refactoring sencilla y progresiva. Parte de una serie de reglas, como la que da título al libro entre otras, para llevarnos de la mano aplicando distintos refactors seguros, por lo que no sería necesario disponer de tests. Con esto, nos ayuda a entender cuál es problema de ese código, por qué y cómo arreglarlo. Para ello nos propone un par de proyectos en TypeScript, lenguaje que toma elementos de otros muchos lenguajes, por lo que no es muy difícil realizar los ejercicios propuestos y llevarlos a tus propios proyectos.

99 bottles of OOP, de Sandi Metz, no es estrictamente un libro sobre refactoring porque su objetivo es enseñar diseño orientado a objetos. Sin embargo, plantea el proceso comenzando por un ejemplo procedural y nos guía en el camino de convertirlo en un programa orientado a objetos, paso a paso y sin romper la funcionalidad. Creo que el término que mejor encaja para eso es refactoring. Además, tienes una edición del libro para PHP, Ruby, JS y Python. Sandi Metz es una de mis autoras favoritas, así que recomiendo a ciegas cualquier cosa que publique, y sus charlas son fantásticas.

Blogs y otros recursos online

Refactoring.guru es un sitio web que, en parte, reproduce muchos elementos de los libros de M. Fowler, algo que en su día les llevó a ciertos problemas relacionados con la propiedad intelectual. El caso es que esta web nos presenta un catálogo de refactorings y code smells muy apañado, con numerosos ejemplos, explicaciones, esquemas e ideas. La verdad es que mola bastante.

The talking bit. El origen de este libro es mi blog personal, que está bastante lleno de contenido técnico relacionado sobre todo con testing, diseño de software y, por supuesto, refactoring. Y está en español, que de recursos en inglés estamos surtidas.

Vídeos

Workflows of refactoring es la charla de Martin Fowler en la que explica cuando se hace el refactoring. De visión obligada. Además, es cortita y muy al punto.

The real secret to refactoring siempre merece la pena seguir el material de David Farley.

Get a Whiff of This es una charla de Sandi Metz en la que explica los code smells y cómo refactorizarlos, partiendo del libro de Fowler.

Refactoring práctico The Talking Bit tiene un canal en Youtube, en el que voy poniendo, cuando me siento suficientemente inspirado y tengo silencio en casa, vídeos en los que explico procesos de refactoring, TDD, etc. Son un apoyo al material publicado en el blog porque en algún momento pensé que los artículos escritos no conseguían transmitir el proceso sobre el terreno. Como punto característico, no se trata precisamente de shorts, prácticamente son sesiones de live coding (a veces corto algún trocito o edito algún error), por lo que tienen una duración larga. Básicamente, soy yo haciendo ejercicios y pensando en voz alta.

Refactoring con calisthenics Esta es otra serie de vídeos en las que aplico las reglas de calistenia de Jeff Way como forma de dirigir el refactor de un código. En concreto de un ejemplo adaptado por Emily Bache, a partir de otro puesto por Fowler en el libro de Refactoring.

Notas

Refactor y rediseño

1El nombre viene del proyecto donde se usó por primera vez.