Hace un par de días hablábamos en twitter sobre la Ley de Demeter, y es una de esas cosas en las que, depende del día, tengo opiniones encontradas, así que me ha parecido una buena escusa para escribir un post y tratar de analizar sus puntos buenos y malos.
La Ley de Demeter
La Ley de Demeter es un principio de diseño que intenta limitar el acoplamiento entre componentes mediante la aplicación de unas reglas muy simples. Cuando hablamos de diseño orientado a objetos, podríamos resumirla de la siguiente forma:
Un método M de un objeto O, sólo puede invocar métodos en los siguientes objetos:
- El propio objeto O, es decir, el objeto que contiene al método.
- Los parámetros que recibe el método.
- Los objetos instanciados por el método, es decir, variables locales creadas en el método.
- Los objetos contenidos directamente en el objeto O.
- Los objetos accesibles globalmente desde el método M.
Dicho así puede resultar un poco confuso, pero la idea es fácil: no hay que encadenar llamadas sobre los valores de retorno de los métodos. Es decir, hay que evitar cosas como ésta:
public class DrawEngine { public void Draw(Canvas canvas, Box box) { canvas.SetColor(box.Border.Color); canvas.DrawRectangle(box.Location.X, box.Location.Y, box.Size.Width, box.Size.Height); canvas.SetColor(box.BackColor); canvas.FillRectangle(box.Location.X, box.Location.Y, box.Size.Width, box.Size.Height); } }
En ese método estamos encadenando accesos a la estructura interna de un objeto Box
para poder pintarlo en pantalla (Box.Location.X
, Box.Border.Color
, etc.). Al hacer esto estamos acoplando el código cliente (el método Draw
) a la estructura interna del objeto Box
, por lo que si en algún momento decidimos cambiar esa estructura, también tendremos que cambiar el código cliente.
La forma directa de evitar esto es encapsular el acceso completo a las propiedades que necesitamos:
public class DrawEngine { public void Draw(Canvas canvas, Box box) { canvas.SetColor(box.BorderColor); canvas.DrawRectangle(box.X, box.Y, box.Width, box.Height); canvas.SetColor(box.BackColor); canvas.FillRectangle(box.X, box.Y, box.Width, box.Height); } } public class Box { public int X { get { return location.X; } } public int Y { get { return location.X; } } public int Width { get { return size.Width; } } public int Height { get { return size.Height; } } public int BorderColor { get { return border.Color; } } // etc... }
Lo malo de esto es que nos obliga a escribir un montón de código aburrido que lo único que hace es redirigir llamadas a los componentes internos del objeto Box
. A cambio, esa capa de indirección nos permite cambiar la estructura interna de Box
sin que se vean afectados sus clientes.
Tell, don’t ask
En general, muchas de las violaciones de la Ley de Demeter podemos verlas como un caso de Feature Envy (Envidia de Características), porque tenemos una clase que intenta saber demasiado sobre otra (envidia el conocimiento que tiene la otra).
Para solventar esto, una buena solución es aplicar el principio tell, don’t ask (pide, no preguntes), que consiste en pasar la lógica al objeto que contiene la información, en lugar de extraer de él la información y procesarla fuera.
Aplicando eso al ejemplo anterior, tendríamos:
public class DrawEngine { public void Draw(Canvas canvas, Box box) { box.Draw(canvas); } } public class Box { public void Draw(Canvas canvas) { this.border.Draw(canvas, this.location, this.size); canvas.SetColor(this.color); canvas.FillRectangle(this.location.X, this.location.Y, this.size.Width, this.size.Height); } } public class Border { public void Draw(Canvas canvas, Point location, Size size) { canvas.SetColor(this.color); canvas.DrawRectangle(location.X, location.Y, size.Width, size.Height); } }
Con este cambio, hemos mejorado nuestro diseño acercando al comportamiento a los datos, dejando todo mucho más encapsulado, fácil de manejar y desacoplado.
Del encapsulado al God Object
Aplicar el principio del tell, don’t ask no está exento de problemas y corremos el riesgo de convertir nuestra clase en una especie de God Object (objeto Dios) que centraliza un montón de operaciones y viola el Principio de Responsabilidad Única.
Supongamos que ahora, además de querer pintar el objeto Box
en pantalla, quisiéramos serializarlo a XML.
Si seguimos con la idea anterior de encapsular el comportamiento en la clase Box
y sus componentes, estaremos cargándolas con una nueva responsabilidad, y ya tendremos una clase que es a la vez responsable de representar una caja, pintarla en pantalla y serializarla a XML.
Es fácil ver que esta técnica no escala muy bien y que, si en nuestro sistema tenemos muchas operaciones distintas sobre un objeto Box
, esa clase se acabará convirtiendo en algo monstruoso.
Conclusiones
La Ley de Demeter y el principio de tell, don’t ask son unas buena guías para intentar diseñar componentes poco acoplados y conseguir un diseño que sea fácil de evolucionar, pero no dejan de ser eso, guías.
En el diseño de sistemas reales es difícil (¿imposible?) encontrar reglas que podamos seguir siempre, y es necesario estar continuamente analizando las ventajas e inconvenientes de aplicarlas.
Lo más importante es tener claro cuáles son los conceptos subyacentes a todas esas leyes y principios que existen sobre el desarrollo de software para ser capaz de comprender qué implicaciones tiene aplicarlos y qué implicaciones tiene saltárselos.
Hola Juanma,
Magnifico post! gracias por aclarar las dudas de una forma tan entendible… hasta yo lo he entendido! :)
Gracias y un saludo.
Excelente post! Muy buena información.
Muy buen artículo y muy interesante, pero no me cuadra una cosa en relación al principio Don’t tell, Ask.
El «problema» (por decir algo) que le veo es que si implementamos el método Draw que acepte un objeto Canvas en todas las clases susceptibles de ser dibujadas crearemos un acoplamiento entre estas clases y la clase Canvas, por lo que por resolver una cosa nos hemos metido en otra…
En este caso concreto creo que lo suyo sería más que pasar la clase Canvas como parámetro a los métodos, definir una interfaz que implemente los métodos públicos de Canvas para ser pintado, que Canvas realice, y pasarle esta como parámetro a todos los métodos Draw y en algún momento indicar que el objeto Draw en este caso es Canvas, con esto estaríamos inyectando la dependencia y nos quitaríamos el acoplamiento con esa clase…
Total, que al final esto de aplicar como debe ser los principio solid es un berenjenal de narices…
Un saludo y muchas gracias por hacer artículos así de interesantes.
Hola Javi,
Lo que comentas es totalmente cierto.
Es el riesgo que indico al final del post, al ir pasando la responsabilidad «hacia dentro», los objetos cada vez implementan más funcionalidad y se introducen más dependencias para implementar es funcionalidad.
Utilizar un interface, como bien señalas, es una forma de disminuir este tipo de acoplamiento, aunque tampoco es la panacea.
Un saludo,
Juanma.
Hola
Me gustó mucho tu artículo.
Quería pasarte un artículo, de alguna forma relacionado con lo que comentas:
Printers Instead of Getters http://www.yegor256.com/2016/04/05/printers-instead-of-getters.html
Muchas gracias.
Saludos,
Manuel