Cuando desarrollamos software, uno de los smells más habituales es el acoplamiento.
El acoplamiento en el desarrollo de software se puede encontrar a diferentes niveles de abstracción: sistemas, capas o entre objetos.
La ley de Demeter nos previene del acoplamiento al nivel más bajo de abstracción, entre objetos.
¿Qué es la Ley de Demeter?
La Ley de Demeter afirma que un módulo no debe conocer las entrañas de los objetos que manipula.
Los objetos cuando no son anémicos, ocultan sus datos y muestran operaciones, lo que significa que un objeto no debe mostrar su estructura interna a través de métodos de acceso ya que, si lo hace, mostraría, no ocultaría su estructura interna.
Esta ley se conoce también como principio de menor conocimiento.
El motivo es debido que no se debería invocar métodos o funciones de objetos devueltos por otros objetos. Es decir, no hables con desconocidos, sólo con conocidos.
El siguiente código parece incumplir la Ley de Demeter.
val area = house.groundFloor.bathRoom.area;
Ese tipo de código suele denominarse choque de trenes ya que se asemeja a un grupo de vagones de tren.
¿Qué resuelve la ley de Demeter?
Cuando se cumple la ley de Demeter aplicando el principio de mínimo conomiento y no se habla con extraños conseguimos reducir el acoplamiento entre clases.
Al reducir el acoplamiento, estamos evitando problemas futuros cuando la estructura interna de un objeto cambie.
¿Cuándo se incumple la ley de Demeter?
Los principios pueden colisionar unos con otros, sobre todo si los malinterpretamos. Lo importante no son los puntos por línea, sino la idea de guardar la privacidad para reducir el acoplamiento.
¿Siempre que veamos trenes en nuestro código se esta incumpliendo la ley de demeter? No.
Si incumple o no la Ley de Demeter depende de si estamos hablando principalmente de objetos con datos y comportamiento o si son simplementte estructuras de datos sin comportamiento.
Si son objetos, debería ocultarse su estructura interna, no mostrarse, y conocer sus detalles internos sería un claro incumplimiento de la Ley de Demeter.
Vamos a ver cuales serían las excepciones.
Excepciones que no incumplen la ley
Existen una seríe de excepciones donde al ver un choque de trenes no estaríamos incumpliendo la ley de Demeter.
Builder
El patrón builder es útil en muchos contextos. Uno por ejemplo son los test unitarios, donde se puede usar para crear diferentes escenarios de prueba.
Su esencia básicamente es la de utilizar una API fluida para ir definiendo el objeto a crear.
Un buen punto para detectar que no incumple la ley de Demeter, es que después de cada función invocada siempre se devuelve el mismo objeto y no un objeto diferente.
val car = House.Builder()
.id("AD556HYNM")
.name("Xurxodev House")
.description("This is my house")
.build()
Estructuras de datos
Si son simples estructuras de datos, mostrarán su estructura interna con naturalidad y la Ley de Demeter no se aplica.
data class House(val id: String val, name: String, val groundFloor: Address ...)
data class Foor(val name: String, val BathRoom: String? ...)
data class BathRoom(val area: Double ...)
¿Cómo soluciono sin incumplo la Ley de Demeter?
Para poder encontrar una solución, antes tienes que hacerte la siguiente pregunta ¿para qué quieres esos datos de la estructura interna?
Tell, don't ask
Si quieres esos datos para realizar una operación en base a ellos, posiblemente conlleve que la clase es totalmente o parcialmente anémica porque lo más lógico sería que la operación debería hacerla la clase que contiene los datos.
La solución a este escenario es un principio que se conoce como Tell, don't ask (Dile, no preguntes).
Este principio indica que cuando queremos que un objeto haga algo, ¿por qué no se lo pedimos directamente en vez de navegar por su estructura?.
Es decir, recuerda que no tenemos que usar los objetos para pedirles cosas y según la información que nos devuelven tomar decisiones, sino que lo que debemos hacer es decirles a los objetos que hagan cosas y estos objetos internamente tomaran sus propias decisiones según de su estructura interna.
Veamos primero un ejemplo con el problema:
class Service {
fun calculateNetAmount(product: Product, offer: Offer): Amount{
return Amount( product.price.subtract(
product.price.multiply(
BigDecimal(offer.percentage()/100))));
}
}
class Product(val price: BigDecimal) {}
class Offer (val percetage: Double) {}
class Amount(val amount: BigDecimal) {}
Tanto Product como Offer, son modelos anémicos, en el sentido de que tienen datos, pero les faltan funciones y por lo tanto comportamiento.
La operación que realiza Service, conoce demasiado de la estructura interna de las clases que utiliza. Se produce un choque de trenes.
Ahora veamos una solución aplicando Tell, don't ask:
class Product(val price: Amount) {
fun applyOffer(offer:Offer){
this.price = price.minus(offer.applyTo(this.price))
}
}
class Offer(private val percentage: Double ) {
/*,...*/
fun applyTo(amount: Amount): Amount {
return amount.calculatePercentage(percentage)
} /*...*/
}
class Amount(val amount: BigDecimal) {
fun calculatePercentage(percentage: Double): Amount {
return Amount(amount.multiply(BigDecimal(percentage / 100)))
}
operator fun minus(other: Amount): Amount {
return Amount(amount.subtract(other.amount))
}
}
Ahora ya no existe choque de trenes. La solución es dotar de comportamiento a los objetos aplicando Tell, don't Ask.
De esta manera dejan de ser anémicos.
Fachadas
Si Tell, don't ask no es la mejor solución para tu contexto, otra solución puede ser crear fachadas que oculten la estructura interna.
Volviendo al ejemplo inicial, sería algo así:
val area = house.barhRomAreaInGroundFloor();
Conclusiones
En este artículo hemos visto como identificar acoplamiento entre objetos, que es el nivel más bajo de abstracción donde puede existir acoplamiento.
Hemos repasado cuando es un problema y cuando no.
Además hemos visto diferentes soluciones que podemos aplicar.
El nivel de acoplamiento entre objetos es el más sencillo de detectar y solucionar.
En la formación de Clean Architecture enseño a eliminar acoplamiento en los niveles de abstracción más complejos: