La Ley de Demeter es otra de esas leyes que está tan de moda, y que aunque no forma parte de los principios SOLID, se puede considerar casi un anexo a los mismos.
Aunque la idea de esta ley puede parecer muy simple y clara, su aplicación es difusa, y por eso quería explicarla con calma hoy aquí.
La problemática
Muchas veces nos pasa que tenemos una clase que a su vez está formada por objetos de otras clases, y esas a su vez por otros objetos… Es normal en orientación a objetos, y está muy bien.
Tiene pinta de significar que estamos creando nuestro código a partir de clases pequeñas que interactúan entre ellas.
Hasta aquí todo bien. El problema surge cuando una de las clases necesita utilizar alguno de los objetos unos cuantos niveles por debajo. Nos liamos la manta a la cabeza y nos dedicamos a crear getters como si no hubiera un mañana. Eso puede ser peligroso.
El típico código que huele bastante mal es algo tal que así:
getX().getY().getZ().doSomething()
Si usáramos código Kotlin sería algo más del estilo:
x.y.z.doSomething()
¿Cuál es el problema aquí? Pues básicamente que estamos acoplando el código a la estructura de las clases que componen la cadena de llamadas.
Si mañana cambia esa estructura en cualquiera de las clases involucradas, este código se ve afectado.
¿Es esto malo? Depende, pero en general sí, porque este código es muy propenso a modificaciones: mil refactorizaciones diferentes pueden resultar en un cambio en el mismo.
Y eso es un claro síntoma de que el código no está bien hecho.
¿Qué es la Ley de Demeter?
La ley de Demeter básicamente es un mecanismo de detección de acoplamiento, y nos viene a decir que nuestro objeto no debería conocer las entrañas de otros objetos con los que interactúa.
Si queremos que haga algo, ¿por qué no se lo pedimos directamente en vez de navegar por su estructura?
Ley de Demeter: un objeto no debería conocer las entrañas de otros objetos con los que interactúa Clic para tuitearEn pro de la pedantería, diré que la Ley de Demeter se cumple cuando, teniendo una función f de una clase C, esa función sólo llama a funciones de:
- C
- Un objecto creado por f
- Un objeto pasado como argumento a f
- Un objeto almacenado en campo de C
(Definición inspirada en la que puedes encontrar en el libro de Clean Code, de Robert C. Martin)
En mi opinión tampoco hace falta ser extremadamente estrictos con estas reglas, sino que hay que ver cuando realmente nos estamos aprovechando del sistema para cargarnos las reglas.
Si estás usando un elegante encapsulamiento de clases, pero luego lo llenas todo de getters, estás reventando ese encapsulamiento.
Una forma fácil de detectar que se está violando esta Ley es encontrarte con muchas llamadas concatenadas. Pero no es una razón 100% fiable (sigue leyendo hasta el final y verás) ni es el único motivo.
El código anterior se podría escribir de la siguiente forma, y seguiría siendo igual de inválido:
val x = getX() val y = x.getY() val z = y.getZ() z.doSomething()
Lo he escrito en Java porque en Kotlin es más difícil enmascarar este problema. Aunque por contrapartida al usar properties en vez de getters también es más fácil que el caso original se escape.
Así que básicamente quédate con la idea de que si estás accediendo a la estructura interna de otra clase para llamara a sus métodos, seguramente estés violando la ley.
¿Cómo soluciono las violaciones de la Ley de Demeter?
A veces me da la sensación de que no doy más que malas noticias, pero no hay una solución única para este tema. Para mí, esta ley es más una alarma de olores de código, que nos dice que algo no está muy bien construido por ahí.
Si necesitas acceder a la estructura de una clase de la que dependes, seguramente sea porque las responsabilidades no están bien repartidas.
Esto no siempre aplica, y depende del tipo de clase que tengamos.
Podemos distinguir dos tipos de clases diferentes: objetos y estructuras de datos: los primeros definen comportamiento y los segundos almacenan estado.
A los primeros es normal pedirles que hagan cosas mientras que a los segundos es más habitual pedirles que nos den cosas.
Es por eso que la aplicación de esta ley a las clases del segundo tipo pierde bastante validez, y en general no tendremos que preocuparnos demasiado por ello.
Pero bueno, sabiendo que no hay una respuesta única a la pregunta de cómo solucionar este asunto, ¿qué opciones tienes?
1. Añadir métodos extra
Esta es la opción más evidente, y la que menos te recomiendo. En vez de tener varias llamadas, dejas que cada objeto haga sus correspondientes subllamadas. Podrías tener algo como esto:
x.doSomething()
Este a su vez dentro llamaría a:
y.doSomething()
Y así sucesivamente. En algún caso puede valer, pero normalmente lo que está haciendo es esconder el problema, no solucionarlo.
2. Arquitectura
Una buena arquitectura juega un papel muy importante en el desacoplamiento de los distintos módulos del software, y por tanto reducirá bastante la posibilidad de violar esta ley.
Normalmente en una arquitectura cada capa tendrá una serie de interfaces con las que comunicarse, y al pensar en esas capas que ocultarán su implementación, estaremos evitando problemas como los que hemos visto antes.
3. Comprender mejor tu dominio
La idea del dominio y en particular de Domain Driven Design está muy bien explicada en los artículos de Carlos Morera sobre este tema. Pero viene a decirnos que nuestro problema se puede sustentar en unos conceptos clave que lo definen, y teniéndolos muy claros todo el proceso de desarrollo se simplifica mucho.
El no entender bien nuestro dominio puede desembocar fácilmente en que no modelicemos bien la aplicación, y por tanto surgirán muchos problemas de este tipo.
Además, Domain Driven Design hace mucho hincapié en los distintos tipos de elementos que nos pueden ayudar a modelar nuestro software, por lo que nos quedará mucho más claro y detallado el tema que hablábamos antes sobre los tipos de clases.
Conclusión
Como todo en este mundillo, es interesante entender el concepto que hay detrás de esta ley, pero no tomársela como algo que no nos podemos saltar. Habrá que estudiar en cada caso si tiene sentido y por qué.
Como pequeña regla, si ves que necesitas acceder a la estructura de un objeto que contiene lógica, seguramente habrá una forma más elegante de hacerlo. Siempre toma las decisiones pensando en si claramente en un futuro es probable que eso genere un problema de acoplamiento.
Pero no olvides que la sobre-ingeniería es tan mala como la completa carencia de ella.
La sobre-ingeniería es tan mala como la completa carencia de ella Clic para tuitear¿Conocías esta ley? ¿Conoces algún sistema más para detectarla y solucionarla? Cuéntame en los comentarios.
No conocía esta ley, pero no me ha quedado muy claro como solucionarlo. Estaría bien ver algún ejemplo de los casos…
Muchas gracias por el aporte.
Ej en C#:
Mal uso:
for (int i = 0; i<4; i++)
perro.GetPata(i).Mueve();
Uso correcto:
perro.Camina();
Nota: El perro sabrá si para caminar, debe mover las patas, arrastrarse o impulsarse con el rabo.
Moraleja: Si quieres que un perro ande, no se lo digas a las patas, símplemente dile al perro que camine.
Buen ejemplo! Muchas gracias por el aporte.
Excelente y divertido ejemplo!
Antonio,
Me parece que hay un pequeño error en el código ilustrativo que propones:
val x = getX()
val y = getY()
val z = getZ()
z.doSomething()
Tendria que ser:
val x = getX()
val y = x.getY()
val z = y.getZ()
z.doSomething()
Gracias por el articulo!
Tienes toda la razón, gracias! Lo modifico.
Gracias por la explicación
Un placer!