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 y ya va por la tercera 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 me di cuenta de que una serie de artículos publicada en el blog podría convertirse fácilmente en ese libro.

Siempre tuve la sensación de que como libro no era ninguna maravilla, pero por lo visto resultó útil a muchas personas, de lo que me alegro. El caso es que poco después de la celebración de la última conferencia 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.

Siendo un poco más sistemático, 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.
  • 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.

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 lo estás haciendo, 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 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 los nuevos cambios. 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: usamos 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 acumulado 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!

Capítulo 1. 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 cuales 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.

¿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

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 seguros, 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 }

Excepciones: este tipo de comentarios tiene su razón de ser cuando no podemos 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);

Más excepciones: 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 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?

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.

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)
2 {
3     $this->normalizeAmountsToTheSameCurrency($dataToCalculate);
4     $initialCalculation = $this->performInitialCalculation($dataToCalcu\
5 late);
6     $transformedResponse = $this->applyTransformation($initialCalculati\
7 on);
8 }

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.

Excepciones: 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 independientes 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 to conform 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. Aún así, debes asegurarte de mantenerlos al día.

Capítulo 2. 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.

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 deberíamos 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 PriceCalculator {
 2     var Discount $rate;
 3     
 4     public price(): float
 5     {
 6        //...
 7        $rate = $this->getRate($product);
 8        $tax = $this->taxRepository->byProduct($product);
 9         
10        $amount = $product->basePrice();
11        
12        $amount = $this->calculate($amount, $rate, $tax);
13        
14        $amount = $amount - $this->calculateDiscount($amount);
15        
16        return $amount;
17     }
18     
19     public discount(float $rate): void
20     {
21         $this->rate = $rate;
22     }
23     
24     private function calculateDiscount(float $price): float
25     {
26         $discount = $price * $this->rate;
27         
28         return $discount;
29     }
30     
31     private function calculate(int $amount, float $rate, float $tax): f\
32 loat
33     {
34         // some complex calculation
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 $rate fuese algún tipo de comisión, $tax resulta bastante obvio y $amount 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 public price(): float
 2 {
 3     //...
 4     $rate = $this->getRate($product);
 5     $tax = $this->taxRepository->byProduct($product);
 6     
 7     $amount = $product->basePrice();
 8    
 9     $amount = $this->calculateFinalConsumerPrice($amount, $rate, $tax);
10    
11     $amount = $amount - $this->calculateDiscount($amount);
12     
13     return $amount;
14 }
15 
16 private function calculateFinalConsumerPrice(int $amount, float $rate, \
17 float $tax): float
18 {
19     // some complex calculation
20 }

Vayamos ahora con $rate. Tras hablarlo con negocio, hemos llegado a la conclusión de que representa el porcentaje de comisión que corresponde al comercial que ha realizado la venta. Podría pasar a llamarse $commissionRate, al igual que el método del cual la obtenemos.

 1 public price(): float
 2 {
 3     //...
 4     $commissionRate = $this->getCommissionRate($product);
 5     $tax = $this->taxRepository->byProduct($product);
 6     
 7     $amount = $product->basePrice();
 8    
 9     $amount = $this->calculateFinalConsumerPrice($amount, $commissionRa\
10 te, $tax);
11 
12     $amount = $amount - $this->calculateDiscount($amount);
13 
14     return $amount;
15 }
16 
17 private function calculateFinalConsumerPrice(int $amount, float $commis\
18 sionRate, float $tax): float
19 {
20     // some complex calculation
21 }

Además, esto era bastante necesario, porque resulta que la clase tiene otro $rate, que es una propiedad que, teniendo el mismo nombre, representa algo completamente distinto, como es un descuento. Tanto $commissionRate como $rate son ratios (proporciones o porcentajes), pero el hecho de que sean el mismo concepto matemático (ratio o proporción), no implica que sean el mismo concepto de negocio. Por supuesto, necesitamos mayor precisión también aquí:

 1 var float $discountRate;
 2 
 3 //...
 4 
 5 public discount(float $discountRate): void
 6 {
 7     $this->discountRate = $discountRate;
 8 }
 9     
10 private function calculateDiscount(float $price): float
11 {
12     $discount = $price * $this->discountRate;
13     
14     return $discount;
15 }

$tax puede mejorar también. Pero, ¿qué nos cuesta hacerlo explícito si queremos decir que se trata del IVA?

 1 public price(): float
 2 {
 3     //...
 4     $commissionRate = $this->getCommissionRate($product);
 5     $vat = $this->taxRepository->byProduct($product);
 6     
 7     $amount = $product->basePrice();
 8    
 9     $amount = $this->calculateFinalConsumerPrice($amount, $commissionRa\
10 te, $vat);
11 
12     $amount = $amount - $this->calculateDiscount($amount);
13 
14     return $amount;
15 }
16 
17 private function calculateFinalConsumerPrice(
18     int $amount, 
19     float $commissionRate, 
20     float $vat
21 ): float {
22     // some complex calculation
23 }

Nombres reutilizados en el mismo scope

Nunca se deben reutilizar nombres en el mismo scope, o sea, dentro de un mismo bloque, función o clase, para representar cosas distintas porque nos lleva a confusión. Los lenguajes más estrictos son capaces de evitar que introduzcas valores de distinto tipo en la misma variable, mientras que otros no te dejarán reasignarla una vez inicializada, pero existen muchos casos en que esa reasignación es posible, al menos si no cambia el tipo de dato.

En nuestro ejemplo, la variable $amount es asignada tres veces y utilizada con significados diferentes.

Voy a eliminar parte del código para que te puedas fijar en algo aparentemente inocente:

 1 public price(): float
 2 {
 3     //...
 4     $amount = $product->basePrice();
 5    
 6     $amount = $this->calculateFinalConsumerPrice($amount, $commissionRa\
 7 te, $vat);
 8     
 9     //...
10     
11     return $amount;
12 }
13 
14 private function calculateFinalConsumerPrice(
15     int $amount, 
16     float $commissionRate, 
17     float $vat
18 ): float {
19     // some complex calculation
20 }

¿Lo has pillado? El método calculateFinalConsumerPrice espera que $amount sea un entero, mientras que devuelve un valor de coma flotante que se asigna de nuevo a $amount. ¿Qué está pasando aquí?

Pues, por ejemplo, podría estar pasando que el precio que contiene $product, esté expresado en céntimos por la razón que sea, mientras que el precio final se va a expresar en euros. De nuevo, el conflicto se puede resolver siendo explícitos sobre lo que la variable realmente contiene o el parámetro exige:

 1 public price(): float
 2 {
 3     //...
 4     $amountInCents = $product->basePrice();
 5    
 6     $amountInEuros = $this->calculateFinalConsumerPrice($amountInCents,\
 7  $commissionRate, $vat);
 8     
 9     //...
10     
11     return $amountInEuros;
12 }
13 
14 private function calculateFinalConsumerPrice(
15     int $amountInCents, 
16     float $commissionRate, 
17     float $vat
18 ): float {
19     // some complex calculation
20 }

Por otro lado, la secuencia de transformaciones que sufre $amountInEuros puede transmitir mensajes confusos. Por un lado, refleja que es un precio base que se transforma por diversas razones (comisiones, impuestos, descuentos), pero, por otro lado, podría tener diversos significados de negocio que en un momento dado necesitaríamos discriminar.

En el primer caso, esta representación puede ser mucho más descriptiva de lo que realmente pasa:

 1 public price(): float
 2 {
 3     //...
 4     $commissionRate = $this->getCommissionRate($product);
 5     $vat = $this->taxRepository->byProduct($product);
 6     
 7     $amountInCents = $product->basePrice();
 8    
 9     $amountInEuros = $this->calculateFinalConsumerPrice($amountInCents,\
10  $commissionRate, $vat);
11 
12     $amountInEuros -= $this->calculateDiscount($amountInEuros);
13 
14     return $amountInEuros;
15 }

Por ejemplo, podríamos necesitar discriminar el precio antes y después de impuestos. O el total de la comisión que se lleva el comercial, porque se han convertido en cuestiones importantes del negocio:

 1 public price(): float
 2 {
 3     //...
 4     $commissionRate = $this->getCommissionRate($product);
 5     $vat = $this->taxRepository->byProduct($product);
 6     
 7     $amountInCentsBeforeTaxes = $product->basePrice();
 8     
 9     $commission = $this->calculateCommission($amountInCentsBeforeTaxes,\
10  $commissionRate);
11    
12     $amountInEuros = $this->calculateFinalConsumerPrice($amountInCentsB\
13 eforeTaxes, $commissionRate, $vat);
14 
15     $amountInEuros -= $this->calculateDiscount($amountInEuros);
16 
17     return $amountInEuros;
18 }

El refactor va aclarando, por una parte, conceptos de negocio, pero también nos permite descubrir que tenemos problemas más profundos en el código.

Por ejemplo, que nos vendría bien utilizar un ValueObject para representar el precio, como Money, incluso aunque al final devolvamos un float para no cambiar la interfaz pública:

 1 public price(): float
 2 {
 3     //...
 4     $commissionRate = $this->getCommissionRate($product);
 5     $vat = $this->taxRepository->byProduct($product);
 6     
 7     $amountBeforeTaxes = Money::fromCents($product->basePrice(), 'EUR');
 8        
 9     $amount = $amountBeforeTaxes
10         ->addRate($commissionRate)
11         ->addRate($vat);
12         
13     $discountedAmount = $amount->substractRate($this->discountRate());
14 
15     return $discountedAmount->amount();
16 }

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 re-evaluar lo que estamos leyendo.

Así, una acción debería representarse siempre mediante un verbo.

Y un concepto, siempre 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. PriceCalculator parece un buen nombre. Es un sustantivo, por lo que se deduce que es un actor que hace algo. Veámosla como interface:

1 interface PriceCalculator {
2     public price(Product $product): float;
3     public discount(float $rate): float;
4 }

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.

Vamos por la más evidente. El método discount en realidad nos sirve para asignar un descuento aplicable a la siguiente operación price. Estamos usando un sustantivo para indicar una acción. La opción más inmediata:

1 interface PriceCalculator {
2     public price(Product $product): float;
3     public setDiscount(float $rate): float;
4 }

Está mejor, pero también podemos ser más fieles al lenguaje de negocio. De hecho, set tiene un significado demasiado genérico y no dice realmente nada:

1 interface PriceCalculator {
2     public price(Product $product): float;
3     public applyDiscount(float $rate): float;
4 }

En cambio, applyDiscount es una clara acción de negocio y no deja muchas dudas en cuanto al significado. Pero todavía podríamos aportar un poco más de precisión, aunque el nombre del parámetro es $rate, nunca se sabe cómo se va a utilizar:

1 interface PriceCalculator {
2     public price(Product $product): float;
3     public applyDiscountRate(float $rate): float;
4 }

Ahora, sí.

¿Y qué decir de price? De nuevo, es un sustantivo que representa una acción, por lo que podríamos cambiarlo.

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 price(Product $product): float;
3     public applyDiscountRate(float $rate): float;
4 }

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

1 $calculatePrice = new CalculatePrice();
2 
3 $calculatePrice->applyDiscountRate($rate);
4 $calculatePrice->price($product);

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

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

Lo que nos deja con esta interfaz:

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

Números mágicos

En este caso 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. Antes:

1 $vatAmount = $amountBeforeTaxes * .21;

Después:

1 $vatAmount = $amountBeforeTaxes * self::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(Nif::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(self::CONFIRM_SEND_EMAIL_MESSAGE, $user->username(),\
5  $user->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 }

Ya que el uso del verbo en pasado debería ser suficiente para entender de un vistazo que está hablando de un event (un mensaje que indica que algo 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 mayores problemas. 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 lo podemos saber 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 }

De Media que representaría a los soportes de contenidos archivados en la biblioteca y que contendría propiedades como un número de registro (el 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.

Capítulo 3. 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.

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.

Otras reglas prácticas que podemos aplicar son:

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.

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

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 if ($selectedPaymentMethod == null) {
 2     $logger = Logger::getInstance();
 3     $logger->debug("Medio de pago desconocido");
 4     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
 5 d() < 745) {
 6         $paymentMethod = PaymentTypes::PAYPAL;
 7     }
 8 } else {
 9     $paymentMethod = $selectedPaymentMethod->getPaymentMethodType()->ge\
10 tPaymentMethodTypeId();
11 }

Podría reescribirse así:

 1 if (null !== $selectedPaymentMethod) {
 2     $paymentMethod = $selectedPaymentMethod->getPaymentMethodType()->ge\
 3 tPaymentMethodTypeId();    
 4 } else {
 5     $logger = Logger::getInstance();
 6     $logger->debug("Medio de pago desconocido");
 7     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
 8 d() < 745) {
 9         $paymentMethod = PaymentTypes::PAYPAL;
10     }
11 }

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 el else y hacer que el código vuelva al nivel de indentación anterior, mejor si es el primero, lo que facilitará la lectura.

Imaginemos que tras el código anterior tenemos un return, no hace falta que sea inmediatamente después:

 1 if (null !== $selectedPaymentMethod) {
 2     $paymentMethod = $selectedPaymentMethod->getPaymentMethodType()->ge\
 3 tPaymentMethodTypeId();    
 4 } else {
 5     $logger = Logger::getInstance();
 6     $logger->debug("Medio de pago desconocido");
 7     if ($order->getDestinationCountry() == Country::FRANCE && $order->i\
 8 d() < 745) {
 9         $paymentMethod = PaymentTypes::PAYPAL;
10     }
11 }
12 
13 // Some more code to get a value for $paymentMethod
14 
15 return $paymentMethod;

En realidad, en la primera rama ya podríamos volver sin problemas porque lo que devolvemos el $paymentMethods, lo que nos permite eliminar la cláusula else, reduciendo la indentación del código.

 1 if (null !== $selectedPaymentMethod) {
 2     $paymentMethod = $selectedPaymentMethod->getPaymentMethodType()->ge\
 3 tPaymentMethodTypeId();
 4     
 5     return $paymentMethod;    
 6 } 
 7 
 8 $logger = Logger::getInstance();
 9 $logger->debug("Medio de pago desconocido");
10 
11 if ($order->getDestinationCountry() == Country::FRANCE && $order->id() \
12 < 745) {
13     $paymentMethod = PaymentTypes::PAYPAL;
14 }
15 
16 // Some more code to get a value for $paymentMethod
17 
18 return $paymentMethod;

Además, no hace falta crear ni poblar una variable temporal, gracias a lo cual podemos devolver directamente la respuesta obtenida, aplicando lo mismo a la condicional que podemos ver al final:

 1 if (null !== $selectedPaymentMethod) {
 2     return $selectedPaymentMethod->getPaymentMethodType()->getPaymentMe\
 3 thodTypeId();
 4 } 
 5 
 6 $logger = Logger::getInstance();
 7 $logger->debug("Medio de pago desconocido");
 8 
 9 if ($order->getDestinationCountry() == Country::FRANCE && $order->id() \
10 < 745) {
11     return PaymentTypes::PAYPAL;
12 }
13 
14 // Some more code to get a value for $paymentMethod

Un uso 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 el algoritmo solo recibe aquellos casos a los que se aplica realmente.

Cláusulas de guarda

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

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

Extraemos toda la estructura a un método privado:

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

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 más limpia y normalizada. Si la aserción no se cumple, se tirará una excepción:

1 Assert::betweenExclusive($parameter, 0, 100)

Una limitación de las aserciones que debemos tener en cuenta es nunca usarlas 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. Aquí lo podemos ver, si el parámetro excede los límites queremos que se ajuste al límite que ha superado:

 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 }

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 bien sea invirtiéndola, bien encapsulándola de modo que se exprese de manera afirmativa.

Con frecuencia, además, la sintaxis de la negación puede hacerlas poco visibles:

1 if (!$selectedPaymentMethod) {
2     return $selectedPaymentMethod->getPaymentMethodType()->getPaymentMe\
3 thodTypeId();
4 } 

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 especialmente difícil de leer:

1 if (null !== $selectedPaymentMethod) {
2     return $selectedPaymentMethod->getPaymentMethodType()->getPaymentMe\
3 thodTypeId();
4 } 

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

1 if ($selectedPaymentMethod) {
2     return $selectedPaymentMethod->getPaymentMethodType()->getPaymentMe\
3 thodTypeId();
4 } 

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->getPaymentMethodType()->getPaymentMe\
3 thodTypeId();
4 } 
5 
6 function userHasSelectedAPaymentMethod($selectedPaymentMethod)
7 {
8     return null !== $selectedPaymentMethod;
9 }

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:

1 if ($order->getDestinationCountry() == Country::FRANCE && $order->id() \
2 < 745) {
3     return PaymentTypes::PAYPAL;
4 }
5 
6 // Some more code to get a value for $paymentMethod

Podría ser un poco más explicativa encapsulada en un método:

 1 if (legacyOrdersWithDestinationFrance($order)) {
 2     return PaymentTypes::PAYPAL;
 3 }
 4 
 5 // Some more code to get a value for $paymentMethod
 6 
 7 private function legacyOrdersWithDestinationFrance($order)
 8 {
 9     return $order->getDestinationCountry() == Country::FRANCE && $order\
10 ->id() < 745;
11 }

Esto deja el bloque de esta manera:

 1 if ($selectedPaymentMethod) {
 2     return $selectedPaymentMethod->getPaymentMethodType()->getPaymentMe\
 3 thodTypeId();
 4 } 
 5 
 6 $logger = Logger::getInstance();
 7 $logger->debug("Medio de pago desconocido");
 8 
 9 if (legacyOrdersWithDestinationFrance($order)) {
10     return PaymentTypes::PAYPAL;
11 }
12 
13 // Some more code to get a value for $paymentMethod
14 
15 private function legacyOrdersWithDestinationFrance($order)
16 {
17     return $order->getDestinationCountry() == Country::FRANCE && $order\
18 ->id() < 745;
19 }

Del singleton que tenemos por ahí no hablaremos en esta ocasión.

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 == PaymentTypes::BANK_TRANSFER) {
 6         return 'pendiente de transferencia';
 7     }
 8     if ($paymentMethod == PaymentTypes::PAYPAL || $paymentMethod == Pay\
 9 mentTypes::CREDIT_CARD) {
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 == PaymentTypes::BANK_TRANSFER) {
15         return 'pendiente de transferencia';
16     }
17     if ($paymentMethod == PaymentTypes::PAYPAL || $paymentMethod == Pay\
18 mentTypes::CREDIT_CARD) {
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 todas 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. Por ejemplo, siguiendo con el ejemplo anterior, este método que hemos extraído:

 1 private function reportForProductInPendingStatus(paymentMethod)
 2 {
 3     if ($paymentMethod == PaymentTypes::BANK_TRANSFER) {
 4         return 'pendiente de transferencia';
 5     }
 6     if ($paymentMethod == PaymentTypes::PAYPAL || $paymentMethod == Pay\
 7 mentTypes::CREDIT_CARD) {
 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 PaymentTypes::BANK_TRANSFER:
 5             return 'pendiente de transferencia';
 6         case PaymentTypes::PAYPAL:
 7         case PaymentTypes::CREDIT_CARD:
 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, 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 sus 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 }

Sólo 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.

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.

Capítulo 4. 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.

Todos los lenguajes de programación vienen de serie con un conjunto de tipos de datos básicos que denominamos primitivos (y en algún caso escalares, cuando no son objetos): boolean, int, float, string…, que utilizamos para representar cosas y operar con ellas. La parte mala es que son tipos genéricos y, a veces, necesitaríamos algo con más significado.

Lo ideal sería poder crear nuestros propios tipos, aptos para el dominio en el que estemos trabajando e incluyendo sus propias restricciones y propiedades. Además, podrían encapsular las operaciones que les sean necesarias. ¿Te suena el concepto? Estamos hablando de value objects.

Los value objects son objetos que representan algún concepto importante en el dominio. Ya hemos hablado un montón de veces de ellos en el blog, por lo que simplemente haré un resumen. Puedes encontrar más detalles y ejemplos en este artículo de Dani Tomé.

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 esa igualdad.
  • Son inmutables, o sea, 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 a

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 tipos escalares 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.

Propiedades múltiples para un solo concepto

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.

 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.

Yo no sé a ti pero a mí esto me pide un 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 }

Builder que podríamos usar así:

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

Gracias a usar el builder podemos ver que hay, 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.

Vamos por partes:

Value object simple

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 value object, 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 value object sencillo:

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

La validación debe hacerse en el constructor, de modo que solo se puedan instanciar PersonName correctos. Supongamos que nuestras reglas son:

  • Name y FirstSurname son obligatorios y no pueden ser un string vacío.
  • LastSurname es opcional.

Al incluir la validación tendremos el siguiente código:

 1 class PersonName
 2 {
 3     /** @var string */
 4     private $name;
 5     /** @var string */
 6     private $firstSurname;
 7     /** @var string */
 8     private $lastSurname;
 9 
10     public function __construct(string $name, string $firstSurname, ?st\
11 ring $lastSurname = null)
12     {
13         if ($name === '' || $firstSurname === '') {
14             throw new InvalidArgumentException('Name and First Surname \
15 are mandatory');
16         }
17         $this->name = $name;
18         $this->firstSurname = $firstSurname;
19         $this->lastSurname = $lastSurname;
20     }
21 }

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.

value object Compuesto

Para crear el VO Address haremos algo parecido y crearemos una clase Address para representar las direcciones de los clientes.

Sin embargo, hemos dicho que podríamos crear un value object en torno al concepto de localidad o código postal, que incluiría el código postal y la ciudad. 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     /** @var string */
 4     private $postalCode;
 5     /** @var string */
 6     private $locality;
 7 
 8     public function __construct(string $postalCode, string $locality)
 9     {
10         $this->isValidPostalCode($postalCode);
11         $this->isValidLocality($locality);
12         
13         $this->postalCode = $postalCode;
14         $this->locality = $locality;
15     }
16 
17     private function isValidPostalCode(string $postalCode) : void
18     {
19         if (\strlen($postalCode) !== 5 || (int) substr($postalCode, 0, \
20 2) > 52) {
21             throw new InvalidArgumentException('Invalid Postal Code');
22         }
23     }
24     
25     private function isValidLocality(string $locality) : void
26     {
27         if ($locality === '') {
28             throw new InvalidArgumentException('Locality should have a \
29 value');
30         }
31     }

La verdad es que podríamos hilar más fino y declarar un VO PostalCode:

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

De modo que Locality quedaría así:

 1 class Locality
 2 {
 3     /** @var PostalCode */
 4     private $postalCode;
 5     /** @var string */
 6     private $locality;
 7 
 8     public function __construct(PostalCode $postalCode, string $localit\
 9 y)
10     {
11         $this->isValidLocality($locality);
12 
13         $this->postalCode = $postalCode;
14         $this->locality = $locality;
15     }
16     
17     private function isValidLocality(string $locality) : void
18     {
19         if ($locality === '') {
20             throw new InvalidArgumentException('Locality should have a \
21 value');
22         }
23     }
24 }

En este caso, no consideramos que PostalCode sea una dependencia de Locality. Estamos hablando de tipos de datos por lo que estos objetos son newables, que es una forma de decir que se instancian a medida que se necesitan.

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

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

Puesto que Locality es un VO no es necesario validarla. Además, aquí no necesitamos saber cómo se construye por lo que nos daría igual si hemos optado por un diseño u otro de la clase, ya que la vamos a recibir construida y Address puede confiar en que funcionará como es debido.

Siempre que un objeto requiere muchos parámetros en su construcción puede ser interesante plantearse si tenemos buenas razones para organizarlos en forma de value objects, 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     /** @var string */
 4     private $street;
 5     /** @var string */
 6     private $streetNumber;
 7     /** @var string */
 8     private $floor;
 9 
10     public function __construct(string $street, string $streetNumber, ?\
11 string $floor)
12     {
13         if ('' === $street || '' === $streetNumber) {
14             throw new InvalidArgumentException('Address should include \
15 street and number');
16         }
17         $this->street = $street;
18         $this->streetNumber = $streetNumber;
19         $this->floor = $floor;
20     }
21 }

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

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

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.

Introduciendo los value objects

Volvamos a Customer. De momento, el hecho de introducir una serie de value objects no afecta para nada al código que tengamos, por lo que podríamos estar creando cada uno de los VO, haciendo commits, mezclando en master 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 $id;
 4     /** @var PersonName */
 5     private $personName;
 6     private $street;
 7     private $streetNumber;
 8     private $floor;
 9     private $postalCode;
10     private $city;
11 
12     public function __construct(
13         string $id,
14         PersonName $personName,
15         string $street,
16         string $streetNumber,
17         string $floor,
18         string $postalCode,
19         string $city
20     ) {
21         $this->id = $id;
22         $this->personName = $personName;
23         $this->street = $street;
24         $this->streetNumber = $streetNumber;
25         $this->floor = $floor;
26         $this->postalCode = $postalCode;
27         $this->city = $city;
28     }
29 
30     public function fullName(): string
31     {
32         return $this->personName->fullName();
33     }
34 
35     public function address(): string
36     {
37         $address = $this->street . ', ' . $this->streetNumber;
38 
39         if ($this->floor) {
40             $address .= ' '. $this->floor;
41         }
42 
43         $address .= $this->postalCode. '-'.$this->city;
44 
45         return $address;
46     }
47 }

Como podemos ver, para empezar el constructor ya es 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     /** @var string */
 4     private $name;
 5     /** @var string */
 6     private $firstSurname;
 7     /** @var string */
 8     private $lastSurname;
 9 
10     public function __construct(string $name, string $firstSurname, ?st\
11 ring $lastSurname = null)
12     {
13         if ($name === '' || $firstSurname === '') {
14             throw new InvalidArgumentException('Name and First Surname \
15 are mandatory');
16         }
17         $this->name = $name;
18         $this->firstSurname = $firstSurname;
19         $this->lastSurname = $lastSurname;
20     }
21 
22     public function fullName(): string
23     {
24         $fullName = $this->name . ' ' . $this->firstSurname;
25         
26         if ($this->lastSurname) {
27             $fullName .= ' ' . $this->lastSurname;
28         }
29         
30         return $fullName;
31     }
32 }

Es por esto que decimos que los value objects “atraen” comportamientos, ya que cualquier cosa que las clases usuarias necesiten puede encapsularse en el propio VO. Si necesitásemos el nombre en un formato apto para listas podríamos hacer lo siguiente:

 1 class PersonName implements PersonNameInterface
 2 {
 3     /** @var string */
 4     private $name;
 5     /** @var string */
 6     private $firstSurname;
 7     /** @var string */
 8     private $lastSurname;
 9 
10     public function __construct(string $name, string $firstSurname, ?st\
11 ring $lastSurname = null)
12     {
13         if ($name === '' || $firstSurname === '') {
14             throw new InvalidArgumentException('Name and First Surname \
15 are mandatory');
16         }
17         $this->name = $name;
18         $this->firstSurname = $firstSurname;
19         $this->lastSurname = $lastSurname;
20     }
21 
22     public function fullName(): string
23     {
24         return $this->name .' ' . $this->surname();
25     }
26 
27     public function listName(): string
28     {
29         return $this->surname() . ', ' . $this->name;
30     }
31 
32     public function surname(): string
33     {
34         $surname = $this->firstSurname;
35 
36         if ($this->lastSurname) {
37             $surname .= ' ' . $this->lastSurname;
38         }
39 
40         return $surname;
41     }
42 
43     public function treatment() : string
44     {
45         return $this->name();
46     }
47 }

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

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 VO Address. Así queda Customer:

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

El método full en Address lo resuelvo mediante un type casting a string de sus componentes, que es una manera sencilla de disponer de su valor en un formato escalar estándar:

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

En este caso necesitaremos:

 1 class StreetAddress
 2 {
 3     /** @var string */
 4     private $street;
 5     /** @var string */
 6     private $streetNumber;
 7     /** @var string */
 8     private $floor;
 9 
10     public function __construct(string $street, string $streetNumber, ?\
11 string $floor)
12     {
13         if ('' === $street || '' === $streetNumber) {
14             throw new InvalidArgumentException('Address should include \
15 street and number');
16         }
17         $this->street = $street;
18         $this->streetNumber = $streetNumber;
19         $this->floor = $floor;
20     }
21 
22     public function __toString(): string 
23     {
24         $fullAddress = $this->street . ' ' . $this->streetNumber;
25         
26         if ($this->floor) {
27             $fullAddress .= ', '. $this->floor;
28         }
29         
30         return $fullAddress;
31     }
32 }

Y también:

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

Así como:

 1 class Locality
 2 {
 3     /** @var PostalCode */
 4     private $postalCode;
 5     /** @var string */
 6     private $locality;
 7 
 8     public function __construct(PostalCode $postalCode, string $localit\
 9 y)
10     {
11         $this->isValidLocality($locality);
12 
13         $this->postalCode = $postalCode;
14         $this->locality = $locality;
15     }
16 
17     private function isValidLocality(string $locality) : void
18     {
19         if ($locality === '') {
20             throw new InvalidArgumentException('Locality should have a \
21 value');
22         }
23     }
24 
25     public function __toString(): string 
26     {
27         return (string) $this->postalCode .'-'.$this->locality;
28     }
29 }

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(new PostalCode($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, lo que nos dice que es bueno tener un único lugar de instanciación de los objetos.

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, que son conceptos relevantes dentro de Customer.

Por otro lado, todo lo que tiene que ver con ellos, Customer se lo delega. 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 los value objects, con lo cual el nuevo comportamiento se extenderá a todas las partes de la aplicación que lo utilicen.

Introduciendo nuevas features a través de los value objects

Vamos a ver cómo la nueva situación en la que nos deja el refactor nos facilita la vida en el futuro. Imaginemos que tenemos que añadir una nueva feature en la aplicación.

Es importante tratar bien a los clientes, por lo que nos han pedido incluir una propiedad de género que permita personalizar el tratamiento que utilizamos en las comunicaciones.

¿A quién pertenecería esa propiedad en nuestro modelo? En el diseño inicial tendríamos que ponerla en Customer, pero ahora podríamos hacerlo en PersonName. Aún mejor: no toquemos nada, o casi nada, de lo que hay.

Supongamos que Customer tiene un método treatment al que recurrimos para montar emails o cartas:

1 public function treatment(): string 
2 {
3     return $this->personName->treatment();
4 }

Primero queremos un nuevo value object, que será Gender:

 1 class Gender
 2 {
 3     /** @var string */
 4     private $gender;
 5 
 6     private const FEMALE = 'female';
 7     private const MALE = 'male';
 8 
 9     private const VALID_GENDERS = [
10         self::FEMALE,
11         self::MALE
12     ];
13     
14     private const TREATMENTS = [
15         self::FEMALE => 'Estimada',
16         self::MALE => 'Estimado'
17     ];
18 
19     public function __construct(string $gender)
20     {
21         if (!\in_array(strtolower($gender), self::VALID_GENDERS, true))\
22  {
23             throw new \InvalidArgumentException('Gender should be one o\
24 f '.implode(', ', self::VALID_GENDERS));
25         }
26 
27         $this->gender = strtolower($gender);
28     }
29 
30     public function treatment(): string
31     {
32         return self::TREATMENTS[$this->gender];
33     }
34 }

Este tipo de value object será un enumerable. Representa conceptos que tienen un número limitado (numerable) de valores. El siguiente paso es hacer que la clase PersonName implemente una interfaz PersonNameInterface:

1 interface PersonNameInterface
2 {
3     public function fullName() : string;
4     public function listName() : string;
5     public function surname() : string;
6     public function treatment(): string;
7 }

Y hacemos que Customer la utilice. Es un cambio pequeño, que nos permitirá usar implementaciones alternativas:

 1 class Customer
 2 {
 3     private string $id;
 4     private PersonNameInterface $personName;
 5     private Address $address;
 6 
 7     public function __construct(
 8         string $id,
 9         PersonNameInterface $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     
27     public function treatment(): string 
28     {
29         return $this->personName->treatment();
30     }
31 }

Ahora, crearemos un tipo de PersonName que sepa algo acerca del género del nombre:

 1 class GenderMarkedPersonName implements PersonNameInterface
 2 {
 3     private Gender $gender;
 4     private PersonName $personName;
 5 
 6     public function __construct(Gender $gender, PersonName $personName)
 7     {
 8         $this->gender = $gender;
 9         $this->personName = $personName;
10     }
11 
12     public function fullName(): string
13     {
14         return $this->personName->fullName();
15     }
16 
17     public function listName(): string
18     {
19         return $this->personName->listName();
20     }
21 
22     public function surname(): string
23     {
24         return $this->personName->surname();
25     }
26 
27     public function treatment() : string
28     {
29         return $this->gender->treatment().' '.$this->personName->treatm\
30 ent();
31     }
32 }

Y ahora, el último cambio, en el Builder usaremos la nueva implementación:

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

Esta modificación ya tiene algo más de calado, pero sigue siendo razonablemente segura. Si te fijas, solo estamos modificando comportamientos del Builder y ahí podemos ser un poco menos estrictos. Teniendo muchísimo rigor, podríamos crear un decorador de CustomerBuilder que crease un Customer cuyo PersonNameInterface fuese un GenderMarkerPersonName, pero ya estaríamos entrando quizá en la sobre-ingeniería.

Fíjate que ahora Customer es capaz de usar el tratamiento adecuado para cada cliente y realmente no lo hemos tenido que tocar. De hecho, hemos añadido la feature añadiendo bastante código, pero con un mínimo riesgo de afectación al resto de la aplicación.

Haciendo balance

Veamos algunos números. Empezamos con dos clases: Customer y CustomerBuilder. ¿Sabes cuántas tenemos ahora? Nada menos que nueve, además de una interfaz. La parte buena es que hemos afectado muy poco al código restante, tan solo en el último paso, cuando hemos tenido que introducir una nueva feature. Pero aquí ya no estamos hablando de refactor.

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.

Capítulo 5. Refactoriza a Enumerables

Que trata sobre cómo gestionar aquellos tipos de datos que tienen valores limitados, algo que solemos encontrar en prácticamente todos los dominios.

En este capítulo profundizaremos en una idea que ya apuntamos en un capítulo anterior: refactorizar introduciendo enumerables. Los enumerables son tipos de datos que cuentan con un número finito de valores posibles.

Supongamos el típico problema de representar los estados de alguna entidad o proceso. Habitualmente usamos un string o un número para ello. La primera opción ayuda a que el valor sea legible por humanos, mientras que la representación numérica puede ser más compacta aunque más difícil de entender.

En cualquier caso, el número de estados es limitado y lo podemos contar. El problema es garantizar la consistencia de los valores que utilicemos para representarlos, incluso entre distintos sistemas.

Y aquí es dónde pueden ayudarnos los Enumerables.

Los Enumerables se modelan como Value Objects. Esto quiere decir que un objeto representa un valor y se encarga de mantener su consistencia, disfrutando de todas las ventajas que señalamos en el capítulo anterior.

En la práctica, además, podemos hacer que los Enumerables nos permitan una representación semántica en el código, aunque internamente transporten valores abstractos, como códigos numéricos, necesarios para la persistencia en base de datos, por ejemplo.

De escalar a enumerable

Empecemos con un caso más o menos típico. Tenemos una entidad con una propiedad que puede tener dos valores, como ‘activo’ y ‘cancelado’. Inicialmente la modelamos con un string, que es como se va a guardar en base de datos, y confiamos en que lo sabremos manejar sin mayores problemas en el código. ¿Qué podría salir mal?

Para empezar, cuando tenemos una variable o propiedad de tipo string, tenemos infinitos valores potenciales de esa variable o propiedad y tan solo queremos usar dos de ellos. Así que, cuando necesitemos usarlo, tendremos que asegurarnos de que solo consideraremos esos dos strings concretos. En otras palabras: tendremos que validarlos cada vez.

Habitualmente también haremos alguna normalización, como poner el string en minúsculas o mayúsculas, para simplificar el proceso de comparación y asegurarnos una representación coherente.

Pero esto debería hacerse cada vez que vamos a utilizar esta variable o propiedad, o al menos, siempre que sepamos que su origen no es confiable, como el input de usuario o una petición a la API o cualquier fuente de datos que no esté en un estado conocido.

En lugar de esto, deberíamos usar un Value Object, como vimos en el capítulo anterior. Realmente, lo único que hace un poco especiales a los Enumerables es el hecho de que el número de valores posibles es limitado lo que nos permite usar algunas técnicas interesantes.

Nuestra primera iteración es simple, definimos una clase Status que contiene un valor string.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class Status
 7 {
 8     /** @var string */
 9     private $status;
10 
11     public function __construct(string $status)
12     {
13         $this->status = $status;
14     }
15 
16 }

Personalmente, me gusta añadir un método __toString a los VO para poder hacer el type cast si lo necesito.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class Status
 7 {
 8     /** @var string */
 9     private $status;
10 
11     public function __construct(string $status)
12     {
13         $this->status = $status;
14     }
15 
16     public function __toString(): string 
17     {
18         return $this->status;
19     }
20 
21 }

Tenemos que definir cuáles son los valores aceptables para este VO, lo cual podemos hacer mediante constantes de clase. Definiremos una para cada valor válido y una extra que será un simple array que los agrupa, lo que nos facilitará la validación.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class Status
 7 {
 8     public const ACTIVE = 'activo';
 9     public const CANCELLED = 'cancelado';
10 
11     private const VALID_VALUES = [
12         self::CANCELLED,
13         self::ACTIVE
14     ];
15     
16     /** @var string */
17     private $status;
18 
19     public function __construct(string $status)
20     {
21         $this->status = $status;
22     }
23 
24     public function __toString(): string
25     {
26         return $this->status;
27     }
28 }

En el ejemplo he puesto los valores aceptados en español y los nombres de las constantes en inglés, que es como haremos referencia a ellos en el código por cuestiones de lenguaje de dominio. Esta diferencia podría darse cuando, por ejemplo, necesitamos interactuar con un sistema legacy en el que esos valores están representados en español y sería más costoso cambiarlo que adaptarnos.

En cualquier caso, lo que importa es que vamos a tener una representación en código de esa propiedad y los valores concretos son detalles de implementación que en unos casos podremos elegir y en otros no.

Por otro lado, está el tema de las constantes públicas. Es una cuestión de conveniencia, ya que nos puede permitir acceder a los valores estándar en momentos en los que no podemos usar objetos mediante llamada estática.

Nuestro siguiente paso debería ser implementar la validación que nos garantice que podemos instanciar solo valores correctos.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 
 8 class Status
 9 {
10     public const ACTIVE = 'activo';
11     public const CANCELLED = 'cancelado';
12 
13     private const VALID_VALUES = [
14         self::CANCELLED,
15         self::ACTIVE
16     ];
17 
18     /** @var string */
19     private $status;
20 
21     public function __construct(string $status)
22     {
23         if (! in_array($status, self::VALID_VALUES, true)) {
24             throw new InvalidArgumentException(sprintf('%s is an invali\
25 d value for status', $status));
26         }
27         $this->status = $status;
28     }
29 
30     public function __toString(): string
31     {
32         return $this->status;
33     }
34 }

Como se puede ver, es muy sencillo, ya que simplemente comprobamos si el valor aportado está en la lista de valores admitidos. Pero, como es un string, podríamos tener algún problema en caso de que nos pasen el dato con alguna mayúscula. En estos casos, no está de más, realizar una normalización básica. Tampoco se trata de arreglar el input externo, pero sí de prevenir alguno de los errores habituales.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 
 8 class Status
 9 {
10     public const ACTIVE = 'activo';
11     public const CANCELLED = 'cancelado';
12 
13     private const VALID_VALUES = [
14         self::CANCELLED,
15         self::ACTIVE
16     ];
17 
18     /** @var string */
19     private $status;
20 
21     public function __construct(string $status)
22     {
23         $status = mb_convert_case($status, MB_CASE_LOWER);
24         
25         if (! in_array($status, self::VALID_VALUES, true)) {
26             throw new InvalidArgumentException(sprintf('%s is an invali\
27 d value for status', $status));
28         }
29         
30         $this->status = $status;
31     }
32 
33     public function __toString(): string
34     {
35         return $this->status;
36     }
37 }

Para terminar con lo básico, necesitamos un método para comprobar la igualdad, así como un método para obtener su valor escalar si fuese preciso.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 
 8 class Status
 9 {
10     public const ACTIVE = 'activo';
11     public const CANCELLED = 'cancelado';
12 
13     private const VALID_VALUES = [
14         self::CANCELLED,
15         self::ACTIVE
16     ];
17 
18     /** @var string */
19     private $status;
20 
21     public function __construct(string $status)
22     {
23         $status = mb_convert_case($status, MB_CASE_LOWER);
24 
25         if (! in_array($status, self::VALID_VALUES, true)) {
26             throw new InvalidArgumentException(sprintf('%s is an invali\
27 d value for status', $status));
28         }
29         $this->status = $status;
30     }
31 
32     public function value(): string 
33     {
34         return $this->status;
35     }
36     
37     public function equals(Status $anotherStatus): bool
38     {
39         return $this->status === $anotherStatus->status;
40     }
41 
42     public function __toString(): string
43     {
44         return $this->value();
45     }
46 
47 }

Y, con esto, ya tenemos un Enumerable.

Bonus points

Hay algunas cosas interesantes que podemos hacer con los enumerables, a fin de que resulten más cómodos y útiles.

Por ejemplo, podemos querer tener named constructors que hagan más explícita la forma de creación.

1 public static function fromString(string $status): Status
2 {
3     return new self($status);
4 }

Puesto que son pocos valores, podríamos permitirnos tener named constructors para crear directamente instancias con un valor determinado:

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 use phpDocumentor\Reflection\Types\Self_;
 8 
 9 class Status
10 {
11     public const ACTIVE = 'activo';
12     public const CANCELLED = 'cancelado';
13 
14     private const VALID_VALUES = [
15         self::CANCELLED,
16         self::ACTIVE
17     ];
18 
19 
20     /** @var string */
21     private $status;
22 
23     private function __construct(string $status)
24     {
25         $status = mb_convert_case($status, MB_CASE_LOWER);
26 
27         if (! in_array($status, self::VALID_VALUES, true)) {
28             throw new InvalidArgumentException(sprintf('%s is an invali\
29 d value for status', $status));
30         }
31         $this->status = $status;
32     }
33 
34     public static function fromString(string $status): Status
35     {
36         return new self($status);
37     }
38 
39     public static function active(): Status
40     {
41         return new self(self::ACTIVE);
42     }
43 
44     public static function cancelled(): Status
45     {
46         return new self(self::CANCELLED);
47     }
48 
49     public function value(): string
50     {
51         return $this->status;
52     }
53 
54     public function equals(Status $anotherStatus): bool
55     {
56         return $this->status === $anotherStatus->status;
57     }
58 
59     public function __toString(): string
60     {
61         return $this->value();
62     }
63 
64 }

Con esto, podemos hacer privado el constructor standard, usando así la clase:

1 $initialStatus = Status::active();
2 
3 $newStatus = Status::cancelled(); 

Enumerables y cambios de estado

Supongamos que una cierta propiedad de una entidad se puede modelar con un enumerable de n elementos con la característica de que solo puede cambiar en una cierta secuencia.

Con frecuencia nos encontramos que esta gestión de estados la realiza la entidad. Sin embargo, podemos delegar en el enumerable buena parte de este comportamiento.

A veces esta secuencia es lineal, indicando que la entidad pasa, a lo largo de su ciclo de vida, por los diferentes estados en un orden prefijado. Por ejemplo, un contrato puede pasar por los estados pre-signed, signed, extended and finalized, pero siempre lo hará en ese orden, por lo que es necesario comprobar que no es posible asignar a un contrato un estado nuevo que sea “incompatible” con el actual.

Otras veces, el orden de la secuencia puede variar, pero solo se puede pasar de unos estados determinados a otros. Por ejemplo, un post de un blog, puede pasar de draft a ready to review, pero no directamente a published, mientras que desde ready to review puede volver a draft, si el revisor no lo encuentra adecuado, o avanzar a published si está listo para ver la luz.

Como hemos dicho, este tipo de reglas de negocio pueden encapsularse en el propio enumerable simplificando así el código de la entidad. Hay muchas formas de hacer esto y en casos complejos necesitaremos hacer uso de otros patrones.

Veamos el ejemplo lineal. Supongamos un ContractStatus que admite tres estados que se suceden en una única secuencia. Podemos tener un método en el Enumerable para avanzar un paso el estado:

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use DomainException;
 7 
 8 class ContractStatus
 9 {
10     public const PRESIGNED = 'presigned';
11     public const SIGNED = 'signed';
12     public const FINALIZED = 'finalized';
13 
14     private const VALID_STATUSES = [
15         self::PRESIGNED,
16         self::SIGNED,
17         self::FINALIZED
18     ];
19     /** @var string */
20     private $status;
21 
22     public function __construct(string $status)
23     {
24         $status = mb_convert_case($status, MB_CASE_LOWER);
25         if (! in_array($status, self::VALID_STATUSES, true)) {
26             throw new \InvalidArgumentException(sprintf('%s status not \
27 valid', $status));
28         }
29         $this->status = $status;
30     }
31 
32     public function status(): string
33     {
34         return $this->status;
35     }
36 
37     public function forward(): ContractStatus
38     {
39         switch ($this->status) {
40             case self::PRESIGNED:
41                 return new self(self::SIGNED);
42             case self::SIGNED:
43                 return new self(self::FINALIZED);
44         }
45 
46         throw new DomainException(
47             sprintf('Can not forward from %s status', $this->status())
48         );
49     }
50 }

Este ejemplo nos permite hacer avanzar el estado de un objeto contrato de este modo. Recuerda que al ser un Value Object el método nos devuelve una nueva instancia de ContractStatus.

1 try {
2     $this->contractStatus = $this->contractStatus->forward();
3 } catch (DomainException $exception) {
4     // Do the right thing to manage exception
5 }

Otra situación interesante se produce cuando necesitamos reasignar el estado del contrato de forma directa. Por ejemplo, debido a errores o tal vez por necesidades de sincronización entre distintos sistemas. En esos caso, podríamos tener (o no) reglas de negocio que permitan ciertos cambios y prohiban otros.

Para nuestro ejemplo vamos a imaginar que un contrato puede volver atrás un paso (de signed a pre-signed y de finalized a signed) o avanzar un paso, como en el método forward.

Esta implementación es bastante tosca, pero creo que representa con claridad la intención. El método changeTo nos permite pasarle un nuevo ContractStatus y nos lo devuelve si el cambio es válido o bien lanza una excepción.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use DomainException;
 7 
 8 class ContractStatus
 9 {
10     public const PRESIGNED = 'presigned';
11     public const SIGNED = 'signed';
12     public const FINALIZED = 'finalized';
13 
14     private const VALID_STATUSES = [
15         self::PRESIGNED,
16         self::SIGNED,
17         self::FINALIZED
18     ];
19     /** @var string */
20     private $status;
21 
22     public function __construct(string $status)
23     {
24         $status = mb_convert_case($status, MB_CASE_LOWER);
25         if (! in_array($status, self::VALID_STATUSES, true)) {
26             throw new \InvalidArgumentException(sprintf('%s status not \
27 valid', $status));
28         }
29         $this->status = $status;
30     }
31 
32     public function status(): string
33     {
34         return $this->status;
35     }
36 
37     public function forward(): ContractStatus
38     {
39         switch ($this->status) {
40             case self::PRESIGNED:
41                 return new self(self::SIGNED);
42             case self::SIGNED:
43                 return new self(self::FINALIZED);
44         }
45 
46         throw new DomainException(
47             sprintf('Can not forward from %s status', $this->status())
48         );
49     }
50 
51     public function changeTo(ContractStatus $newContractStatus): Contra\
52 ctStatus
53     {
54     
55         switch ($this->status) {
56             case self::PRESIGNED:
57                 if ($newContractStatus->status() !== self::SIGNED) {
58                     $this->failWhenChangeIsNotAllowed($newContractStatu\
59 s);
60                 }
61                 break;
62             case self::FINALIZED:
63                 if ($newContractStatus->status() !== self::SIGNED) {
64                     $this->failWhenChangeIsNotAllowed($newContractStatu\
65 s);
66                 }
67                 break;
68             default:
69                 if ($newContractStatus->status() === self::SIGNED) {
70                     $this->failWhenChangeIsNotAllowed($newContractStatu\
71 s);
72                 }
73         }
74 
75         return $newContractStatus;
76     }
77 
78     private function failWhenChangeIsNotAllowed(ContractStatus $newCont\
79 ractStatus): void
80     {
81         throw new DomainException(
82             sprintf(
83                 'Change form %s to %s is not allowed',
84                 (string)$this,
85                 (string)$newContractStatus
86             )
87         );
88     }
89 }

En esencia, el método changeTo valida que el estado se pueda cambiar teniendo en cuenta el estado actual. La idea de fondo es aplicar el principio Tell, don't ask, de modo que no le preguntemos al contrato por su estado, ni a ContractStatus por su valor, si no que le decimos que cambie a un nuevo estado si es posible. En caso de fallo, ya tomaremos nosotros las medidas necesarias.

1 try {
2     $newStatus = new ContractStatus('finalized');
3     
4     $this->contractStatus = $this->contractStatus->changeTo($newStatus);
5 } catch (DomainException $exception) {
6     // Do the right thing to manage exception
7 }

Enumerables como traductores

¿Y qué ocurre si tenemos que interactuar con distintos sistemas que representan el mismo significado con distintos valores? Podría ocurrir que uno de los sistemas lo hiciese con enteros de modo que necesitamos alguna traducción.

Un enfoque pragmático, cuando las combinaciones de valores/versiones son reducidas, sería incorporar esa capacidad al propio Enumerable, mediante un named constructor específico y un método para obtener esa versión del valor.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 
 8 class Status
 9 {
10     public const ACTIVE = 'activo';
11     public const CANCELLED = 'cancelado';
12 
13     private const VALID_VALUES = [
14         self::CANCELLED,
15         self::ACTIVE
16     ];
17 
18     private const LEGACY_MAP = [
19         101 => self::CANCELLED,
20         200 => self::ACTIVE
21     ];
22 
23     /** @var string */
24     private $status;
25 
26     public function __construct(string $status)
27     {
28         $status = mb_convert_case($status, MB_CASE_LOWER);
29 
30         if (! in_array($status, self::VALID_VALUES, true)) {
31             throw new InvalidArgumentException(sprintf('%s is an invali\
32 d value for status', $status));
33         }
34         $this->status = $status;
35     }
36 
37     public static function fromString(string $status): Status
38     {
39         return new self($status);
40     }
41 
42     public static function fromLegacy(int $status): Status
43     {
44         return new self(self::LEGACY_MAP[$status]);
45     }
46 
47     public function toLegacy(): int
48     {
49         return array_flip(self::LEGACY_MAP)[$this->status];
50     }
51 
52     public function value(): string
53     {
54         return $this->status;
55     }
56 
57     public function equals(Status $anotherStatus): bool
58     {
59         return $this->status === $anotherStatus->status;
60     }
61 
62     public function __toString(): string
63     {
64         return $this->value();
65     }
66 }

Añadiendo rigor al enumerable

Aunque la solución que acabamos de ver resulta práctica en ciertos casos, lo cierto es que no es precisamente rigurosa al mezclar responsabilidades.

Nos hace falta algún tipo de traductor:

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class LegacyStatusTransformer
 7 {
 8     private const LEGACY_MAP = [
 9         '101' => Status::CANCELLED,
10         '200' => Status::ACTIVE
11     ];
12 
13     public function fromLegacy(string $status): Status
14     {
15         return new Status(self::LEGACY_MAP[$status]);
16     }
17 
18 
19     public function toLegacy(Status $status): string
20     {
21         return (string)array_flip(self::LEGACY_MAP)[$status->value()];
22     }
23 }

Capítulo 6. Aplica Tell, Don’t Ask y la Ley de Demeter

En el que tratamos sobre la redistribución de responsabilidades. Porque en un sistema orientado a objetos la pregunta no es: ¿cómo hay que hacer esto?, sino: ¿quién debería estar haciendo esto? Y, por tanto, lo que buscamos es la forma de mover las responsabilidades y comportamientos a los objetos a los que pertenecen naturalmente.

Hasta ahora, hemos trabajado refactors muy orientados a mejorar la expresividad del código y a la organización de unidades de código. En este capítulo vamos a trabajar en cómo mejorar las relaciones entre 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. Vamos a centrarnos en dos principios que son bastante fáciles de aplicar y que mejorarán la inteligibilidad y la posibilidad de testear nuestro código. Se trata de Tell, Don’t Ask y la Ley de Demeter.

Primero haremos un repaso y luego los veremos en acción.

Tell, don’t ask

La traducción de este enunciado a español sería algo así como “Ordena, 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 debe ser responsable de su estado.

Veamos un ejemplo bastante absurdo, pero que lo deja claro.

Supongamos que tenemos una clase Square que representa un cuadrado y queremos poder calcular su área.

1 $square = new Square(20);
2 
3 $side = $square->side();
4 
5 $area = $side**2;

Si aplicamos el principio Tell, Don’t Ask, el cálculo del área estaría en la clase Square:

1 $square = new Square(20);
2 
3 $area = $square->area();

Mejor, ¿no? Veamos por qué.

En el dominio de las figuras geométricas planas, el área o superficie es una propiedad que tienen todas ellas y que, a su vez, depende de otras que son su base y su altura, que en el caso del cuadrado coinciden. La función para determinar el área ocupada por una figura plana depende de cada figura específica.

Posiblemente, estés de acuerdo en que al modelar este comportamiento lo pondríamos en la clase de cada figura desde el primer momento, lo que seguramente nos llevaría a una interfaz.

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

La primera razón es que toda la información necesaria para calcular el área está en la clase, por lo que tiene todo el sentido del mundo que el conocimiento preciso para calcularla también esté allí. Se aplica el principio de Cohesión y el principio de Encapsulamiento, manteniendo juntos los datos y las funciones que procesan esos datos.

Una segunda razón es más pragmática: si el conocimiento para calcular el área está en otro lugar, como un servicio, cada vez que necesitemos incorporar una nueva clase al sistema, tendremos que modificar el servicio para añadirle ese conocimiento, rompiendo el principio Abierto/Cerrado.

En tercer lugar, el testing se simplifica. Es fácil hacer tests de las clases que representan las figuras. Por otro lado, el testing de las otras clases que las utilizan también se simplifica. Normalmente esas clases usarán el cálculo del área como una utilidad para llevar a cabo sus propias responsabilidades que es lo que queremos saber si hacen correctamente.

Siguiendo el principio Tell, Don’t Ask movemos responsabilidades a los objetos a los que pertenecen.

Ley de Demeter

La Ley de Demeter1 también se conoce como Principio de menor conocimiento y, más o menos, dice que un objeto no debería conocer la organización interna de los otros objetos con los que colabora.

Siguiendo la Ley de Demeter, como veremos, un método de una clase solo puede usar 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.

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $discountPct = $product->currentPromotion()->discountPct();
4     //...
5 }

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 el descuento.

¿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 conocimiento excesivo nos dice que estamos ante una violación de la Ley de Demeter.

Puedes ponerlo así, pero sigue siendo el mismo problema:

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $promotions = $product->currentPromotion();
4     $discountPct = $promotion->discountPct();
5     //...
6 }

Una justificación que se aduce en ocasiones es que puesto que sabes qué clase de objeto devuelve el objeto intermedio, entonces puedes saber su interfaz. Sin embargo, lo que conseguimos es una dependencia oculta de entre dos objetos que no tienen una relación directa. Esto es:

El objeto A usa el objeto B para obtener y usar el objeto C.

El objeto A depende del objeto C, pero no hay nada en A que nos diga que existe esa dependencia.

Es posible aplicar varias soluciones. La más adecuada depende de varios factores.

Encapsular en nuevos métodos

En algunos casos, se trataría de aplicar el principio Tell, Don’t Ask. Esto es. A veces, la responsabilidad de ofrecernos una cierta respuesta encajaría en el objeto intermedio, por lo que podríamos encapsular la cadena de llamadas. Veámoslo con un ejemplo similar:

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $basePrice = $product->family()->basePrice();
4     $unitPrice = $basePrice + $product->extraPrice();
5     return $unitPrice * $units;
6 }

En este caso, resulta razonable pensar que la estructura del precio de un producto es algo propio del producto, y los usuarios del objeto no tienen por qué conocerla. En un primer paso, aplicamos la Ley de Demeter haciendo que el objeto Price sea el que obtiene el precio base, sin que la calculadora tenga que saber de dónde se obtiene.

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $basePrice = $product->basePrice();
4     $unitPrice = $basePrice + $product->extraPrice();
5     return $unitPrice * $units;
6 }

En ese caso, Product utiliza su colaborador Family para obtener el valor que devolver en basePrice.

En el segundo paso, aplicamos Tell, Don't Ask, ya que realmente estamos pidiéndole cosas a Product que puede hacer por sí mismo.

1 public function calculatePrice(Product $product, int $units): float
2 {
3     return $product->unitPrice() * $units;
4 }

Reasignación de responsabilidades

El primer ejemplo sobre descuentos es un poco más delicado que el que acabamos de ver:

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $discountPct = $product->currentPromotion()->discountPct();
4     //...
5 }

No está tan claro que las posibles promociones formen parte de la estructura de precios de un producto. Las promociones son seguramente una responsabilidad de Marketing y los productos y precios son de Ventas. Puesto que no queremos tener dos razones para cambiar Producto, lo lógico es que las promociones estén en otra parte.

Dicho de otro modo, no tiene mucho sentido que un producto conozca cuáles son las promociones que se le aplican en el contexto de una campaña de marketing, que son puntuales y limitadas en el tiempo, mientras que la estructura de precio es algo permanente.

Tiene más sentido que la responsabilidad de las promociones esté en otro lugar. Podría ser algo así:

1 public function calculatePrice(Product $product, int $units): float
2 {
3     $discountPct = $this->getPromotions->forProduct($product);
4     $price =  $product->unitPrice() * $units;
5     return $price - ($price * $discountPct / 100);
6 }

La clase que contiene el método calculatePrice tendría un colaborador que le proporciona los descuentos disponibles para un producto.

En resumidas cuentas, el código que incumple la Ley de Demeter tratando con objetos que no conoce directamente puede estar revelando problemas más profundos en el diseño y que hay que solucionar. Estos problemas se manifiestan en un acoplamiento fuerte entre objetos que tienen una relación escasa entre sí.

Capítulo 7. Refactoriza de single return a return early

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.

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.

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:

  • 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 fuerza 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 parece que 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 arbitrarias. Su objetivo 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 encajan cosas como 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.

Ejemplo básico

Hace un par de años comencé a practicar un ejercicio para estudiar algoritmos y estructuras de datos, reproduciéndolos en PHP usando metodología TDD. El código visto ahora está un poco pobre, pero me viene bien porque he encontrado varios ejemplos de single return y otros puntos de mejora.

En primer lugar, vamos a ver un caso en el que podemos refactorizar un single return muy evidente, pero también uno que no lo es tanto:

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

El primer paso es invertir la condicional, para ver la rama más corta en primer lugar. Aquí se ve claramente que cada una de las ramas implica un cálculo diferente de 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. Al hacer esto, también podemos eliminar el uso de la variable temporal $sorted que es innecesaria.

 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 sub-lista correspondiente y se van ordenando esas sub-listas 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 }

Otro ejemplo

En este caso es un Binary Search Tree, en el que se nota que no tenía muy claro el concepto de return early o, al menos, no lo había aplicado hasta sus últimas consecuencias, por lo que el código no mejora apenas:

  1 <?php
  2 
  3 namespace Dsa\Structures;
  4 
  5 class BinarySearchTree
  6 {
  7     /**
  8      * @var BinarySearchNode
  9      */
 10     private $root;
 11 
 12     public function insert($value)
 13     {
 14         $new = new BinarySearchNode($value);
 15         if (! $this->root) {
 16             $this->root = $new;
 17         } else {
 18             $this->insertNew($this->root, $new);
 19         }
 20     }
 21 
 22     public function insertNew(
 23         BinarySearchNode $current,
 24         BinarySearchNode $new
 25     ) {
 26         if ($new->getValue() < $current->getValue()) {
 27             if (! $current->getLeft()) {
 28                 $current->setLeft($new);
 29             } else {
 30                 $this->insertNew($current->getLeft(), $new);
 31             }
 32         } else {
 33             if (! $current->getRight()) {
 34                 $current->setRight($new);
 35             } else {
 36                 $this->insertNew($current->getRight(), $new);
 37             }
 38         }
 39     }
 40 
 41     public function isInTree($value)
 42     {
 43         if (! $this->root) {
 44             return false;
 45         }
 46 
 47         return $this->contains($this->root, $value);
 48     }
 49 
 50     public function contains(
 51         BinarySearchNode $current = null,
 52         $value
 53     ) {
 54         if (! $current) {
 55             return false;
 56         }
 57 
 58         return $this->findNode($current, $value) ? true : false;
 59     }
 60 
 61     public function getParentValueOf($value)
 62     {
 63         if ($value == $this->root->getValue()) {
 64             return null;
 65         }
 66 
 67         return $this->findParent($this->root, $value)->getValue();
 68     }
 69 
 70     private function findParent(
 71         BinarySearchNode $current,
 72         $value
 73     ) {
 74         if ($value < $current->getValue()) {
 75             if (! $current->getLeft()) {
 76                 return null;
 77             } elseif ($current->getLeft()->getValue() == $value) {
 78                 return $current;
 79             } else {
 80                 return $this->findParent($current->getLeft(), $value);
 81             }
 82         } else {
 83             if (! $current->getRight()) {
 84                 return null;
 85             } elseif ($current->getRight()->getValue() == $value) {
 86                 return $current;
 87             } else {
 88                 return $this->findParent($current->getRight(), $value);
 89             }
 90         }
 91     }
 92 
 93     private function findNode(
 94         BinarySearchNode $current = null,
 95         $value
 96     ) {
 97         if (! $current) {
 98             return false;
 99         }
100 
101         if ($current->getValue() == $value) {
102             return $current;
103         } elseif ($value < $current->getValue()) {
104             return $this->findNode($current->getLeft(), $value);
105         } else {
106             return $this->findNode($current->getRight(), $value);
107         }
108     }
109 }

Empecemos mejorando el método insert:

1 public function insert($value)
2 {
3     $new = new BinarySearchNode($value);
4     if (! $this->root) {
5         $this->root = $new;
6     } else {
7         $this->insertNew($this->root, $new);
8     }
9 }

Que podría quedar así:

 1 public function insert($value): void
 2 {
 3     $new = new BinarySearchNode($value);
 4     if (! $this->root) {
 5         $this->root = $new;
 6         return;
 7     }
 8 
 9     $this->insertNew($this->root, $new);
10 }

Al método insertNew le sobra indentación:

 1 public function insertNew(
 2     BinarySearchNode $current,
 3     BinarySearchNode $new
 4 ) {
 5     if ($new->getValue() < $current->getValue()) {
 6         if (! $current->getLeft()) {
 7             $current->setLeft($new);
 8         } else {
 9             $this->insertNew($current->getLeft(), $new);
10         }
11     } else {
12         if (! $current->getRight()) {
13             $current->setRight($new);
14         } else {
15             $this->insertNew($current->getRight(), $new);
16         }
17     }
18 }

Empezamos aplicando el return early una vez:

 1 public function insertNew(
 2     BinarySearchNode $current,
 3     BinarySearchNode $new
 4 ) {
 5     if ($new->getValue() < $current->getValue()) {
 6         if (! $current->getLeft()) {
 7             $current->setLeft($new);
 8             
 9             return;
10         }
11 
12         $this->insertNew($current->getLeft(), $new);
13     } else {
14         if (! $current->getRight()) {
15             $current->setRight($new);
16             
17             return;
18         }
19 
20         $this->insertNew($current->getRight(), $new);
21     }
22 }

Y una segunda vez:

 1 public function insertNew(
 2     BinarySearchNode $current,
 3     BinarySearchNode $new
 4 ): void {
 5     if ($new->getValue() < $current->getValue()) {
 6         if (! $current->getLeft()) {
 7             $current->setLeft($new);
 8             
 9             return;
10         }
11 
12         $this->insertNew($current->getLeft(), $new);
13         return;
14     }
15 
16     if (! $current->getRight()) {
17         $current->setRight($new);
18         
19         return;
20     }
21 
22     $this->insertNew($current->getRight(), $new);
23 }

Otro lugar que necesita un arreglo es el método findParent. Aquí hemos usado return early, pero no habíamos sabido aprovecharlo:

 1 private function findParent(
 2     BinarySearchNode $current,
 3     $value
 4 ) {
 5     if ($value < $current->getValue()) {
 6         if (! $current->getLeft()) {
 7             return null;
 8         } elseif ($current->getLeft()->getValue() == $value) {
 9             return $current;
10         } else {
11             return $this->findParent($current->getLeft(), $value);
12         }
13     } else {
14         if (! $current->getRight()) {
15             return null;
16         } elseif ($current->getRight()->getValue() == $value) {
17             return $current;
18         } else {
19             return $this->findParent($current->getRight(), $value);
20         }
21     }
22 }

Al hacerlo, nos queda un código más limpio:

 1 private function findParent(
 2     BinarySearchNode $current,
 3     $value
 4 ) {
 5     if ($value < $current->getValue()) {
 6         if (! $current->getLeft()) {
 7             return null;
 8         }
 9 
10         if ($current->getLeft()->getValue() === $value) {
11             return $current;
12         }
13 
14         return $this->findParent($current->getLeft(), $value);
15     }
16 
17     if (! $current->getRight()) {
18         return null;
19     }
20 
21     if ($current->getRight()->getValue() === $value) {
22         return $current;
23     }
24 
25     return $this->findParent($current->getRight(), $value);
26 }

Todos estos refactors se pueden hacer sin riesgo con las herramientas de Intentions (comando + return) de PHPStorm que nos ofrece la inversión de if/else (flip), y la separación de flujos (split workflows) cuando son posibles. En todo caso, estas clases estaban cubiertas por tests y estos siguen pasando sin ningún problema.

Finalmente, arreglamos findNode, que estaba así:

 1     private function findNode(
 2         BinarySearchNode $current = null,
 3         $value
 4     ) {
 5         if (! $current) {
 6             return false;
 7         }
 8 
 9         if ($current->getValue() == $value) {
10             return $current;
11         } elseif ($value < $current->getValue()) {
12             return $this->findNode($current->getLeft(), $value);
13         } else {
14             return $this->findNode($current->getRight(), $value);
15         }
16     }

Y quedará así:

 1 private function findNode(
 2     BinarySearchNode $current = null,
 3     $value
 4 ) {
 5     if (! $current) {
 6         return false;
 7     }
 8 
 9     if ($current->getValue() === $value) {
10         return $current;
11     }
12 
13     if ($value < $current->getValue()) {
14         return $this->findNode($current->getLeft(), $value);
15     }
16 
17     return $this->findNode($current->getRight(), $value);
18 }

Capítulo 8. Dónde poner el conocimiento

En el que se continúan ideas expuestas en el Capítulo 6 acerca de cómo reasignar responsabilidades en el código.

En anteriores capítulos hemos propuesto refactorizar código a Value Objects o aplicar principios como la ley de Demeter o Tell, don’t ask para mover código a un lugar más adecuado. En este capítulo vamos a analizarlo desde un punto de vista más general.

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.

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

  • 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.

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

En los objetos a los que pertenece

El patrón GRASP Information Expert nos viene perfecto aquí. Este patrón nos dice que un objeto debería ser la fuente de verdad de todo lo que tiene que ver consigo mismo. 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.

En ese sentido, los value objects y entidades, de los que hemos hablado tantas veces, son lugares ideales para encapsular conocimiento. Veamos un ejemplo:

Supongamos que en nuestro negocio estamos interesados en ofrecer ciertos productos o ventajas a usuarios cuya cuenta de correo pertenezca a ciertos dominios, por ejemplo perteneciente a su cuenta corporativa. El correo electrónico es, pues, un concepto importante del negocio y lo representamos mediante un value object:

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 use InvalidArgumentException;
 7 
 8 class Email
 9 {
10     private string $email;
11 
12     public function __construct(string $email)
13     {
14         $this->email = $email;
15     }
16     
17     public static function valid(string $email)
18     {
19         if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
20             throw new InvalidArgumentException(sprintf('%s is not valid\
21  email.', $email));
22         }
23         return new self($email)
24     }
25 
26     public function value(): string
27     {
28         return $this->email;
29     }
30 
31     public function __toString(): string
32     {
33         return $this->value();
34     }
35 }

En un determinado servicio verificamos que el dominio del correo electrónico del usuario se encuentra dentro de la lista de dominios beneficiados de este tratamiento especial.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class OfferPromotion
 7 {
 8     public function applyTo(Order $order)
 9     {
10         [, $domain] = explode('@', $order->customerEmail());
11 
12         if (in_array($domain, $this->getPromotionDomains->execute(), tr\
13 ue)) {
14             $order->applyPromotion($this);
15         }
16     }
17 }

El problema aquí es que el servicio tiene que ocuparse de obtener el dominio de la dirección de correo, cosa que no tendría que ser de su incumbencia. La clase Email nos está pidiendo a gritos convertirse en la experta en calcular la parte del dominio del correo:

 1 <?php
 2 <?php
 3 declare(strict_types=1);
 4 
 5 namespace App\Domain;
 6 
 7 use InvalidArgumentException;
 8 
 9 class Email
10 {
11     private string $email;
12 
13     public function __construct(string $email)
14     {
15         $this->email = $email;
16     }
17     
18     public static function valid(string $email)
19     {
20         if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
21             throw new InvalidArgumentException(sprintf('%s is not valid\
22  email.', $email));
23         }
24         return new self($email)
25     }
26 
27     public function domain(): string
28     {
29         [, $domain] = explode('@', $this->email);
30 
31         return $domain;
32     }
33 
34     public function value(): string
35     {
36         return $this->email;
37     }
38 
39     public function __toString(): string
40     {
41         return $this->value();
42     }
43 }

Nos basta con esto para hacer más expresivo nuestro servicio, pero aún podemos hacerlo mejor:

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class OfferPromotion
 7 {
 8     public function applyTo(Order $order)
 9     {
10         $email = $order->customer()->email();
11 
12         if (in_array($email->domain(), $this->getPromotionDomains->exec\
13 ute(), true)) {
14             $order->applyPromotion($this);
15         }
16     }
17 }

Reglas de negocio como Specification

El ejemplo anterior es una primera aproximación a cómo mover el conocimiento. En este caso no se trata tanto de la regla de negocio como de un requisito para poder implementarla.

Podríamos decir que la regla de negocio implica distintos conocimientos. En términos de negocio nuestro ejemplo se enunciaría como “todos los clientes cuyo dominio de correo esté incluido en la lista tienen derecho a tal ventaja cuando realicen tal acción”. Técnicamente, implica saber sobre usuarios y sus emails, y saber extraer su dominio de correo para saber si está incluido en tal lista.

Desde el punto de vista del negocio la regla relaciona clientes, seleccionados por una característica, con una ventaja que les vamos a otorgar.

Ese conocimiento se puede encapsular en una Specification, que no es más que un objeto que puede decidir si otro objeto cumple una serie de condiciones. El objeto en que estamos interesadas es Customer y para el contexto de esta regla, nos interesa poder preguntarle por su dominio de correo.

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class BelongsToDomainEligibleForPromotion
 7 {
 8     public $domains = [
 9         'example.com',
10         'example.org'
11     ];
12     
13     public function isSatisfiedBy(Customer $customer): bool
14     {
15         return $this->isEmailEligibleForPromotion($customer->email());
16     }
17     
18     private function isEmailEligibleForPromotion($domain) {
19         if (in_array($email->domain(), $this->domains, true)) {
20             return true;
21         }
22         
23         return false;
24     }
25 }

Ahora el conocimiento de la regla de negocio se encuentra en un solo lugar y lo puedes reutilizar allí donde lo necesites2

 1 <?php
 2 declare(strict_types=1);
 3 
 4 namespace App\Domain;
 5 
 6 class OfferPromotion
 7 {
 8     public function applyTo(Order $order)
 9     {
10         $eligibleForPromotion = new BelongsToDomainEligibleForPromotion\
11 ();
12         
13         if ($eligibleForPromotion->isSatisfiedBy($order->customer())) {
14             $order->applyPromotion($this);
15         }
16     }
17 }

No solo eso, sino que incluso nos permite escribir mejor el servicio al expresar las relaciones correctas: en este caso la regla de negocio se basa en una propiedad de los clientes y no de los pedidos, aunque luego se aplique el resultado a los pedidos o al cálculo de su importe.

Sobre el patrón Specification puedes encontrar más información en este artículo

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

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.

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.

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 y JS. 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.

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

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

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

2Una objeción que se puede poner a este código es que instanciamos la Specification. Normalmente, lo mejor sería inyectar en el servicio una factoría de Specification para pedirle las que necesitemos y que sea la factoría la que gestione sus posibles dependencias.