El nombre de la cosa
En el que Adso y Guillermo… ejem, en el que discutimos sobre la necesidad de poner los nombres adecuados a las cosas del código, a fin de que se entienda qué demonios pasaba en aquel momento por nuestra cabeza, o la de la autora del código que tenemos que intervenir.
Probablemente en ningún lugar como el código los nombres configuran la realidad. Escribir código implica establecer decenas de nombres cada día, para identificar conceptos y procesos. Una mala elección de nombre puede condicionar nuestra forma de ver un problema de negocio. Un nombre ambiguo puede llevarnos a entrar en un callejón sin salida, ahora o en un futuro no muy lejano. Pero un nombre bien escogido puede ahorrarnos tiempo, dinero y dificultades.
Notas de la segunda edición
En este capítulo hemos cambiado los ejemplos para que los nombres originales sean mucho menos expresivos. De este modo, se entiende mejor la necesidad del refactor, y también se entiende mejor lo que hace la clase Calculator que hemos usado como ejercicio.
Símbolos con nombres
Un trozo de código debería poder leerse como una especie de narrativa, en la cual cada palabra expresase de forma unívoca un significado. También de forma ubicua y coherente, es decir, que el mismo símbolo debería representar el mismo concepto en todas partes del código.
¿Cuándo refactorizar nombres?
La regla de oro es muy sencilla: cada vez que al leer una línea de código tenemos que pararnos a pensar qué está diciendo lo más probable sea que debamos cambiar algún nombre.
Este es un ejemplo de un código en el que nos encontramos con unos cuantos problemas de nombres, algunos son evidentes y otros no tanto:
1 class Item {
2 private $val;
3
4 public function __construct($val) {
5 $this->val = $val;
6 }
7
8 public function getVal() {
9 return $this->val;
10 }
11 }
12
13 class Calculator {
14 private $rate1;
15 private $rate2;
16
17 public function __construct($rate1, $rate2) {
18 $this->rate1 = $rate1;
19 $this->rate2 = $rate2;
20 }
21
22 public function calc($item) {
23 $val = $item->getVal();
24 $val = $this->applyRate2($val);
25 $val = $this->applyRate1($val);
26 return $val;
27 }
28
29 private function applyRate2($val) {
30 return $val * (1 + $this->rate2);
31 }
32
33 private function applyRate1($val) {
34 return $val * (1 - $this->rate1);
35 }
36 }
Por supuesto, en este ejemplo hay algunos errores más aparte de los nombres. Pero hoy solo nos ocuparemos de estos. Vamos por partes.
Nombres demasiado genéricos
Los nombres demasiado genéricos requieren el esfuerzo de interpretar el caso concreto en que se están aplicando. Además, en un plano más práctico, resulta difícil localizar una aparición específica del mismo que tenga el significado deseado.
¿De dónde vienen los nombres demasiado genéricos? Normalmente, vienen de estadios iniciales del código, en los que probablemente bastaba con ese término genérico para designar un concepto. Con el tiempo, ese concepto evoluciona y se ramifica a medida que el conocimiento de negocio avanza, pero el código puede que no lo haya hecho al mismo ritmo, con lo que llega un momento en que este no es reflejo del conocimiento actual que tenemos del negocio.
Calculate… what? Exactamente, ¿qué estamos calculando aquí? El código no lo refleja. Podría ocurrir, por ejemplo, que $rate1 fuese algún tipo de descuento, $rate2 podría ser una comisión o impuestos y $val parece claro que es algo así como el precio de tarifa de algún producto o servicio, sea lo que sea que vende esta empresa. Es muy posible que este método lo que haga sea calcular el precio final para el consumidor del producto. ¿Por qué no declararlo de forma explícita?
1 class Calculator {
2 private $rate1;
3 private $rate2;
4
5 public function __construct($rate1, $rate2) {
6 $this->rate1 = $rate1;
7 $this->rate2 = $rate2;
8 }
9
10 public function finalPrice($item) {
11 $val = $item->getVal();
12 $val = $this->applyRate2($val);
13 $val = $this->applyRate1($val);
14 return $val;
15 }
16
17 private function applyRate2($val) {
18 return $val * (1 + $this->rate2);
19 }
20
21 private function applyRate1($val) {
22 return $val * (1 - $this->rate1);
23 }
24 }
Vamos a revisar los distintos nombres que se están usando en el código para representar los conceptos que se manejan en esta calculadora de precios. Puesto que tenemos bastante claro que $val es el precio del producto, podemos hacerlo explícito.
1 class Calculator {
2 private $rate1;
3 private $rate2;
4
5 public function __construct($rate1, $rate2) {
6 $this->rate1 = $rate1;
7 $this->rate2 = $rate2;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->getVal();
12 $price = $this->applyRate2($price);
13 $price = $this->applyRate1($price);
14 return $price;
15 }
16
17 private function applyRate2($price) {
18 return $price * (1 + $this->rate2);
19 }
20
21 private function applyRate1($price) {
22 return $price * (1 - $this->rate1);
23 }
24 }
La clase Item, que representa el producto o servicio que estamos vendiendo nos proporciona ese precio base e, igualmente, deberíamos hacerlo explícito.
1 class Item {
2 private $price;
3
4 public function __construct($price) {
5 $this->basePrice = $price;
6 }
7
8 public function basePrice() {
9 return $this->basePrice;
10 }
11 }
Con estos cambios de nombres, debería haber quedado más claro qué es lo que está pasando.
1 class Calculator {
2 private $rate1;
3 private $rate2;
4
5 public function __construct($rate1, $rate2) {
6 $this->rate1 = $rate1;
7 $this->rate2 = $rate2;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->basePrice();
12 $price = $this->applyRate2($price);
13 $price = $this->applyRate1($price);
14 return $price;
15 }
16
17 private function applyRate2($price) {
18 return $price * (1 + $this->rate2);
19 }
20
21 private function applyRate1($price) {
22 return $price * (1 - $this->rate1);
23 }
24 }
Nombres ambiguos
Hay dos propiedades en la calculadora que tienen el mismo nombre: $rate1 y $rate2.
Técnicamente, son correctos, ya que $rate nos sugiere un porcentaje o proporción, algo que podemos confirmar al leer los métodos en los que se aplican. Pero, ¿qué concepto de negocio representan?
Sabemos que se aplican dos, pero no sabemos qué representan. Uno de ellos se resta y el otro se suma. El que se resta, puede ser un descuento, mientras que el que se suma, podría tratarse de un impuesto o una comisión. Debería ser obvio que necesitamos clarificarlo.
Tras hablarlo con negocio, hemos llegado a la conclusión de $rate1 representa un descuento y $rate2 un impuesto. Lo adecuado, en este caso, será poner nombres explícitos.
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->basePrice();
12 $price = $this->applyRate2($price);
13 $price = $this->applyRate1($price);
14 return $price;
15 }
16
17 private function applyRate2($price) {
18 return $price * (1 + $this->tax);
19 }
20
21 private function applyRate1($price) {
22 return $price * (1 - $this->discount);
23 }
24 }
Con este refactor ya hemos ganado mucho en expresividad, pero el término Rate se utiliza en varios nombres compuestos, por lo que no hemos terminado aún. El siguiente paso sería cambiar applyRate1 y applyRate2 por algo más descriptivo:
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->basePrice();
12 $price = $this->applyTax($price);
13 $price = $this->applyDiscount($price);
14 return $price;
15 }
16
17 private function applyTax($price) {
18 return $price * (1 + $this->tax);
19 }
20
21 private function applyDiscount($price) {
22 return $price * (1 - $this->discount);
23 }
24 }
Aplicando este refactor hemos conseguido mucha más claridad y legibilidad. Incluso puede que hayamos hecho aflorar un bug. Si te fijas, se aplican los descuentos tras aplicar los impuestos. En muchos países, los impuestos se aplican sobre el precio final, por lo que deberíamos cambiar el orden de aplicación de los métodos applyTax y applyDiscount.
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->basePrice();
12 $price = $this->applyDiscount($price);
13 $price = $this->applyTax($price);
14 return $price;
15 }
16
17 private function applyTax($price) {
18 return $price * (1 + $this->tax);
19 }
20
21 private function applyDiscount($price) {
22 return $price * (1 - $this->discount);
23 }
24 }
Esto es muy interesante porque nos demuestra como los nombres ambiguos puede llevarnos a errores de lógica. Al emplear el mismo término $rate para referirnos a dos conceptos completamente distintos, hemos podido confundirnos y aplicar los descuentos antes de los impuestos, lo que podría llevar a problemas legales o a pérdidas económicas. Ciertamente, sería posible prevenirlo con un buen test, que nos habría indicado el error cometido, pero, y si no tenemos un test que cubra ese caso concreto, ¿cómo lo detectaríamos?
Pero es que, además, al eliminar la ambigüedad de los nombres, reducimos la dificultad y el tiempo que nos llevaría interpretar el código. No solo para nosotras, sino también para cualquier persona que pueda tener que trabajar con él en el futuro.
Nombres reutilizados en el mismo scope
Aunque ahora tenemos el código en un estado mucho mejor, todavía tenemos un aspecto que no está realmente bien. La variable $price es actualizada constantemente y, no solo cambia de valor, sino que cambia de significado. En un momento dado, $price es el precio base del producto, en otro es el precio con el descuento aplicado y en otro es el precio con el impuesto aplicado.
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $price = $item->basePrice();
12 $price = $this->applyDiscount($price);
13 $price = $this->applyTax($price);
14 return $price;
15 }
16
17 private function applyTax($price) {
18 return $price * (1 + $this->tax);
19 }
20
21 private function applyDiscount($price) {
22 return $price * (1 - $this->discount);
23 }
24 }
El principal inconveniente de esta forma de programar es que tiene un coste de cambio alto en el futuro. Si hubiese que introducir algún paso nuevo en el cálculo del precio final, habría que modificar todos los lugares en los que se actualiza la variable $price. Además, el código es más difícil de comprender, ya que no es fácil saber en qué estado se encuentra $price en cada momento.
Podrías argumentar que se trata de una variable temporal y de que, en último término, lo que buscamos es el resultado final de todas las transformaciones. Por ejemplo, podríamos hacer algo así, y ni siquiera necesitaríamos la variable $price:
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $price = $this->applyTax(
12 $this->applyDiscount(
13 $item->basePrice()
14 )
15 );
16 return $price;
17 }
18
19 private function applyTax($price) {
20 return $price * (1 + $this->tax);
21 }
22
23 private function applyDiscount($price) {
24 return $price * (1 - $this->discount);
25 }
26 }
Bien, esta solución es interesante, pero a primera vista ya parece más difícil de leer. No solo eso, al igual que la solución inicial nos vamos a encontrar con problemas en caso de que en algún momento necesitemos introducir un cambio.
De entrada, sería más sencillo bautizar cada paso del cálculo con un nombre que refleje su significado. Por ejemplo:
1 class Calculator {
2 private $discount;
3 private $tax;
4
5 public function __construct($discount, $tax) {
6 $this->discount = $discount;
7 $this->tax = $tax;
8 }
9
10 public function finalPrice($item) {
11 $basePrice = $item->basePrice();
12 $discountedPrice = $this->applyDiscount($basePrice);
13 $priceAfterTax = $this->applyTax($discountedPrice);
14 return $priceAfterTax;
15 }
16
17 private function applyTax($price) {
18 return $price * (1 + $this->tax);
19 }
20
21 private function applyDiscount($price) {
22 return $price * (1 - $this->discount);
23 }
24 }
Más allá de los nombres: cuando aparecen conceptos
Por otro lado, este tipo de patrones en los que vamos cambiando el valor de una variable con cálculos que la toman como argumento de entrada, nos debería sugerir la presencia de un concepto que estaría mejor representado con un objeto. Como podemos ver, todas las operaciones se realizan sobre un precio, por lo que podríamos encapsularlo en un objeto Price. Y una vez que tenemos un objeto, lo más adecuado sería que este se encargase de aplicar los descuentos y los impuestos.
1 class Price {
2 private $amount;
3
4 public function __construct($amount) {
5 $this->amount = $amount;
6 }
7
8 public function applyTax($tax) {
9 $this->amount *= (1 + $tax);
10 }
11
12 public function applyDiscount($discount) {
13 $this->amount *= (1 - $discount);
14 }
15
16 public function->amount() {
17 return $this->amount;
18 }
19 }
20
21 class Calculator {
22 private $discount;
23 private $tax;
24
25 public function __construct($discount, $tax) {
26 $this->discount = $discount;
27 $this->tax = $tax;
28 }
29
30 public function finalPrice($item) {
31 $price = new Price($item->basePrice());
32 $price->applyDiscount($this->discount);
33 $price->applyTax($this->tax);
34 return $price->amount();
35 }
36 }
Este refactor que acabamos de mostrar va más allá del alcance de este capítulo, pero nos ha servido para mostrar que cuando mejoramos los nombres, es muy fácil que afloren conceptos interesantes que no teníamos bien representados con valores primitivos. En capítulos posteriores examinaremos cómo podemos introducir estos conceptos en nuestro código.
Tipo de palabra inadecuada
Los símbolos que, de algún modo, contradicen el concepto que representan son más difíciles de procesar, generalmente porque provocan una expectativa que no se cumple y, por tanto, debemos reevaluar lo que estamos leyendo. Por ejemplo:
- Una acción debería representarse siempre mediante un verbo.
- Un concepto, mediante un sustantivo.
A su vez, nunca nos sobran los adjetivos para precisar el significado del sustantivo, por lo que los nombres compuestos nos ayudan a representar con mayor precisión las cosas.
Volvamos al ejemplo. Calculator parece un buen nombre. PriceCalculator sería aún mejor, ya que hace explícito el hecho de que calcula precios. Es un sustantivo, por lo que se deduce que es un actor que hace algo. Veámosla como interface:
1 interface PriceCalculator {
2 public finalPrice(Item $item): float;
3 }
Obviamente, este refactor es un poco más arriesgado. Vamos a tocar una interfaz pública, pero también es verdad que con los IDE modernos este tipo de cambios es razonablemente seguro.
finalPrice es un sustantivo, pero en realidad ¿no representa una acción? ¿No sería mejor calculateFinalPrice?
1 interface PriceCalculator {
2 public calculateFinalPrice(Item $item): float;
3 }
Por un lado, es cierto que parece más imperativo. Estamos diciendo algo así como: “Calculadora: calcula el precio final”. No deja lugar a dudas sobre lo que hace. En el lado negativo, resulta un nombre redundante, a la par que largo.
Pero antes… Volvamos un momento a la clase. PriceCalculator, ¿es un actor o una acción? A veces tendemos a ver los objetos como representaciones de objetos del mundo real. Sin embargo, podemos representar acciones y otros conceptos con objetos en el código. Esta forma de verlo puede cambiar por completo nuestra manera de hacer las cosas.
Supongamos entonces, que consideramos que PriceCalculator no es una cosa, sino una acción:
1 interface CalculatePrice {
2 public calculateFinalPrice(Product $product): float;
3 }
Tal y como está ahora, expresar ciertas cosas resulta extraño:
1 $calculatePrice = new CalculatePrice();
2
3 $calculatePrice->calculateFinalPrice($product);
Pero podemos imaginarlo de otra forma mucho más fluida:
1 $calculatePrice = new CalculatePrice();
2
3 $calculatePrice->finalForProduct($product);
Lo que nos deja con esta interfaz:
1 interface CalculatePrice {
2 public finalForProduct(Product $product): float;
3 }
Este cambio de nombre resulta interesante, pero también tenemos que valorarlo en su contexto. Que los objetos tengan nombres de acciones puede ser muy válido para representar casos de uso, pero no tanto para representar entidades de negocio. En este caso, si lo entendemos como una entidad de o un proceso de negocio, PriceCalculator es un nombre mucho más adecuado.
Números mágicos
A veces no se trata estrictamente de refactorizar nombres, sino de bautizar elementos que están presentes en nuestro código en forma de valores abstractos que tienen un valor de negocio que no ha sido hecho explícito.
Poniéndoles un nombre, lo hacemos. Lo que antes era:
1 $vatAmount = $amountBeforeTaxes * .21;
En la línea anterior, .21 es un número mágico. No sabemos qué significa, pero podemos intuir que se trata de un impuesto y deberíamos hacerlo explícito.
Después:
1 const VAT_RATE = .21;
2 $vatAmount = $amountBeforeTaxes * VAT_RATE;
Convertir estos valores en constantes con nombre hace que su significado de negocio esté presente, sin tener que preocuparse de interpretarlo. Además, esto los hace reutilizables a lo largo de todo el código, lo que añade un plus de coherencia.
Así que, cada vez que encuentres uno de estos valores, hazte un favor y reemplázalo por una constante. Por ejemplo, los naturalmente ilegibles patrones de expresiones regulares:
1 $isValidNif = preg_match('/^[0-9XYZ]\d{7}[^\dUIOÑ]$/', $nif);
2
3 // vs
4
5 $isValidNif = preg_match(VALID_NIF_PATTERN, $nif);
O los patrones de formato para todo tipo de mensajes:
1 $mensaje = sprintf('¿Enviar un mensaje a %s en la dirección %s?', $user\
2 ->username(), $user->email());
3
4 $mensaje = sprintf(CONFIRM_SEND_EMAIL_MESSAGE, $user->username(), $user\
5 ->email());
Nombres técnicos
Personalmente, me gustan poco los nombres técnicos formando parte de los nombres de variables, clases, interfaces, etc. De hecho, creo que en muchas ocasiones condicionan tanto el naming, que favorecen la creación de malos nombres.
Ya he hablado del problema de entender que los objetos en programación tienen que ser representaciones de objetos del mundo real. Esa forma de pensar nos lleva a ver todos los objetos como actores que hacen algo, cuando muchas veces son acciones.
En ocasiones, es verdad que tenemos que representar ciertas operaciones técnicas, que no todo va a ser negocio, pero eso no quiere decir que no hagamos las cosas de una manera elegante. Por ejemplo:
1 interface BookTransformer
2 {
3 public function transformToJson(Book $book): string;
4 public function transformFromJson(string $bookDto): Book;
5 }
6
7 // vs
8
9 interface TransformBook
10 {
11 public function toJson(Book $book): string;
12 public function fromJson(string $bookDto): Book;
13 }
En cambio, en el dominio me choca ver cosas como:
1 class BookWasPrintedEvent implements DomainEvent
2 {
3 }
4
5 // vs
6
7 class BookWasPrinted implements DomainEvent
8 {
9 }
En este ejemplo, el uso del verbo en pasado debería ser suficiente para entender de un vistazo que está hablando de un evento, que no es otra cosa que un mensaje que indica que algo interesante ha ocurrido.
Es cierto que incluir algunos apellidos técnicos a nuestros nombres puede ayudarnos a localizar cosas en el IDE. Pero hay que recordar que no programamos para un IDE.
Refactor de nombres
En general, gracias a las capacidades de refactor de los IDE o incluso del Buscar/Reemplazar en proyectos, realizar refactors de nombres es bastante seguro.
Variables locales en métodos y funciones. Cambiarlas no supone ningún problema, pues no afectan a nada que ocurra fuera de su ámbito.
Propiedades y métodos privados en clases. Tampoco suponen ningún problema al no afectar a nada externo a la clase.
Interfaces públicas. Aunque es más delicado, los IDE modernos deberían ayudarnos a realizarlos sin riesgos importantes. La mayor dificultad me la he encontrado al cambiar nombres de clases, puesto que el IDE aunque localiza y cambia correctamente sus usos, no siempre identifica objetos relacionados, como los tests.
El coste de un mal nombre
Imaginemos un sistema de gestión de bibliotecas que, inicialmente, se creó para gestionar libros. Simplificando muchísimo, aquí tenemos un concepto clave del negocio:
1 class Book
2 {
3 private $id;
4 private $title;
5 private $author;
6 private $editor;
7 private $year;
8 private $city;
9 }
Con el tiempo la biblioteca pasó a gestionar revistas. Las revistas tienen número, pero tal vez en su momento se pensó que no sería necesario desarrollar una especialización:
1 class Book
2 {
3 private $id;
4 private $title;
5 private $author;
6 private $editor;
7 private $year;
8 private $city;
9 private $issue;
10 }
Y aquí comienza un desastre que solo se detecta mucho tiempo después y que puede suponer una sangría, quizá lenta pero constante, de tiempo, recursos y, en último término, dinero para los equipos y empresas.
La modificación de la clase Book hizo que esta pasara a representar dos conceptos distintos, pero quizá se consideró que era una ambigüedad manejable: un compromiso aceptable.
Claro que la biblioteca siguió evolucionando y con el avance tecnológico comenzó a introducir nuevos tipos de objetos, como CD, DVD, libros electrónicos, y un largo etcétera. En este punto, el conocimiento que maneja negocio y su representación en el código se han alejado tanto que el código se ha convertido en una pesadilla: ¿cómo sabemos si Book se refiere a un libro físico, a uno electrónico, a una película en DVD, a un juego en CD? Solo podemos saberlo examinando el contenido de cada objeto Book. Es decir: el código nos está obligando a pararnos a pensar para entenderlo. Necesitamos refactorizar y reescribir.
Es cierto que, dejando aparte el contenido, todos los objetos culturales conservados en una biblioteca comparten ese carácter de objeto cultural o soporte de contenidos. CulturalObject se nos antoja un nombre demasiado forzado, pero Media resulta bastante manejable:
1 class Media
2 {
3 private $id;
4 private $signature;
5 private $registeredSince;
6 private $status;
7 }
Media representaría a los soportes de contenidos archivados en la biblioteca y que contendría propiedades como un número de registro (id), la signatura topográfica (que nos comunica su ubicación física) y otros detalles relacionados con la actividad de archivo, préstamo, etcétera.
Pero esa clase tendría especializaciones que representan tipos de medios específicos, con sus propiedades y comportamientos propios.
1 class Book extends Media
2 {
3 }
4
5 class Review extends Media
6 {
7 }
8
9 class ElectronicBook extends Media
10 {
11 }
12
13 class Movie extends Media
14 {
15 }
Podríamos desarrollar más el conocimiento de negocio en el código, añadiendo interfaces. Por ejemplo, la gestión del préstamo:
1 interface Lendable
2 {
3 public function lend(User $user): void;
4 public function return(DateTimeInterface $date): void;
5 }
Pero el resumen es que el hecho de no haber ido reflejando la evolución del conocimiento del negocio en el código nos lleva a tener un sobre-coste en forma de:
- El tiempo y recursos necesarios para actualizar el desarrollo a través de reescrituras.
- El tiempo y recursos necesarios para mantener el software cuando surgen problemas derivados de la mala representación del conocimiento.
- Las pérdidas por no ingresos debidos a la dificultad del software de adaptarse a las necesidades cambiantes del negocio.
Por esto, preocúpate por poner buenos nombres y mantenerlos al día. Va en ello tu salario.
Resumen del capítulo
Los nombres de los diversos objetos y mensajes que viven en un programa deberían ser precisos y reflejar el lenguaje del dominio que se trate. Un buen nombre es aquel que nos permite entender de un vistazo qué hace un objeto o qué representa un mensaje. Un mal nombre, por el contrario, nos obliga a pararnos a pensar y a interpretar lo que estamos leyendo.