Aplica Tell, Don’t Ask

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

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

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

Notas de la segunda edición

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

Tell, don’t ask

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Y una nueva modificación en AreaCalculator:

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

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

Aquí tenemos el cuadrado:

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

El triángulo:

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

Y el círculo:

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

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

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

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

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

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

Resumen del capítulo

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

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