Un modelo sólo debe hablar con su asociación inmediata – Law of Demeter con Rails


books

Una característica muy interesante que tiene Ruby on Rails, son las  asociaciones de Active Record, que a mi modo de ver nos permite de alguna forma el enlazar unos modelos con otros y en particular con las vistas. Sin embargo, también podemos cometer, a pesar de que esta funcionalidad tiene su alcance, puede hacernos complicada la refactorización y que comentamos errores.

En esta ocasión voy a hablar de “Law of Demeter”, que hace tiempo tenía ganas de comentar sobre todo algunos de sus aspectos y confusiones, que yo mismo he llegado a tener y que me gustaría compartir. Cuando la escuché hace tiempo ya, de un compañero de trabajo, Javier Ramirez @supercoco9, que por otro lado no quiero dejar pasar la ocasión para hacerte una sugerencia si eres desarrollador, echa un vistazo a su nuevo proyecto que mola un montón teowaki.com, que además junto con otro compañero que tuve el placer de trabajar en el mismo proyecto, Diego Rodríguez @diec123 (no sé si Diego pensará lo mismo🙂 ) lo han puesto en marcha y que me han comentado en muchas ocasiones, cosas que han hecho y cómo las han hecho, se aprende mucho hablando con ellos, gracias a ambos por esos buenos momentos!!.

Dicho esto, voy a tratar de explicar lo que es Law of Demeter. Bajo mi punto de vista, no es una idea en si misma que podamos decir lo buena que es, es una ley que deberíamos tener en consideración. El caso es que partimos de la base en la que consideramos: “que un modelo sólo debe hablar con su asociación inmediata” y no deberíamos encontrarnos con casos en los que un modelo hable con la asociación de la asociación y además saber la propiedad de la asociación, se trata de un caso de acoplamiento.

Por otro lado he visto ejemplos en los que podríamos quedarnos en la superficie de Demeter y en reconocer únicamente, si el código cumple con Demeter mediante la búsqueda de más de un “punto”, es decir, algo así como:

@invoice.customer.address.street

y la definición de clases tenemos:

class Address < ActiveRecord::Base
     belongs_to :customer
end

class Customer < ActiveRecord::Base
     has_one :address
     has_many :invoices
end

class Invoice < ActiveRecord::Base
    belongs_to :customer
end

Aplicando esta delegación en su lugar, podemos llegar a  reducir los “puntos” que decíamos antes en uno nada más, haciendo: @invoice.customer_street. Pero ¿Cómo lo hemos conseguido? pues simplemente hacer una definición de un método en nuestra clase Invoice que sea:

def customer_street
 customer.street
end

Creando este u otros métodos que necesitemos dentro de la clase Invoice, es un enfoque equivocado ya que si tenemos muchos métodos de este estilo y en los casos que tengamos a futuro cambios, todos estos métodos de contenedor tendríamos que mantenerlos. Claro que podríais estar pensando que es menos costoso que cambiar las referencias a @invoice.customer.address.street en todo el código, pero creo que bajo mi punto de vista y aún así, deberíamos evitar.

Pero afortunadamente, Ruby on Rails dispone de delegate, un método proporciona un acceso directo para indicar que uno o más métodos que se van a crear en el objeto, en realidad están proporcionados por un objeto relacionado. El uso de este método delegado:

class Invoice < ActiveRecord::Base 
     belongs_to :customer 

     delegate :street :to => :customer, :prefix => true
end

Pero déjame preguntarte, ¿Crees que con esta solución hemos resuelto nuestro problema? o mejor dicho ¿Crees que hemos conseguido una solución? Bueno en parte si, si nos quedamos en que aplicar Demeter es únicamente encontrar la repetición de más de una vez los “puntos”, pero realmente, yo diría que la solución tiene una idea errónea de entender hasta dónde podemos llegar. Te explico, la delegación es una técnica eficaz para evitar el caso que no cumpla con la regla de “Law of Demeter”, pero sólo por comportamiento, no por atributos.

Para explicarlo mejor, voy a poner el ejemplo Bock, David. “The Paperboy, The Wallet, and The Law Of Demeter”. El repartidor tiene que cobrar el dinero a sus clientes y cada cliente guarda su dinero en la cartera, de partida, se entiende la idea. Bien, partimos de las clases siguientes:

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet
end

class PaperBoy
  def collector_money(customer, amount)
    if customer.wallet.cash < ammount
      NotEnoughMoney
    else
      customer.wallet.cash -= amount
      @collected += amount
    end
  end
end

Desde el diseño de clases, el repartidor, no debería coger el dinero de la cartera de un cliente, lógico ¿verdad? además queda un poco feo si lo hace tal como sería en la vida real. Pues este caso, es un claro ejemplo de incumplimiento de “Law of Demeter”. Volviendo a la forma que hemos hablado inicialmente para reconocer este tipo de error, podemos ver que tenemos dos puntos en “customer.wallet.cash”.pero llegado a este punto, ¿cómo podríamos cambiar la delegación de atributos y no aplicar una solución como la hemos visto anteriormente? Veamos la definición de clases nuevamente:

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  #Nota: en este caso estamos poniendo un ejemplo de delegación por atributos
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collector_money(customer, amount)
    if customer.cash < ammount
      NotEnoughMoney
    else
      customer.cash -= amount
      @collected += amount
    end
  end
end

En esta nueva declaración de clases estamos cambiando ligeramente la clase Curtomer, ya que un cliente cuenta ahora con dinero en cash, que simplemente delega por atributo al método cash en @wallet.cash. Ahora en el método collector_money de la clase Paperboy, no tenemos dos puntos “customer.wallet.cash”, sólo tenemos un único “punto” en “customer.cash”. Bueno, hemos llegado a una solución, pero ¿Realmente crees que esta delegación ha resuelto nuestro problema? yo diría que no, por el momento nos queda un paso más aún, espera.

Fíjate en el comportamiento, el repartidor de periódicos, sigue llegando directamente a la cartera de un cliente para obtener dinero en efectivo customer.cash -= amount. Eso no no debería ser así, pero sin embargo, si en lugar de delegar atributos, lo que intentamos hacer es delegar el comportamiento, podríamos llegar de una mejor forma a una solución y con un mejor diseño OO. Vemos la nueva definición:

class Wallet
  attr_accessor :cash
  def remove_cash(amount)
     NotEnoughMoney if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet

  #Nota: en este caso estamos poniendo el ejemplo de delegación 
  # por comportamiento
  def pay(amount)
    @wallet.remove_cash(amount)
  end
end
class Paperboy
  def collector_money(customer, damount)
    @collected += customer.pay(amount)
  end
end

Fíjate ahora como hemos hecho para que la delegación sea mucho mejor que las anteriores, más limpia. El método de pago pay de la clase Customer, simplemente delega en el método de def pay(amount), con el argumento sobre el método de pago y en la llamada desde la clase Paperboy hacemos la llamada con el parámetro :

@collected += customer.pay(amount)

Es el método pay de la clase Custumer, es una simple delegación, además deberíamos tener la premisa de que nuestro repartidor, debería saber lo menos posible sobre el cliente, lo importante para el repartidor, sería saber si el cliente tiene un método disponible para pagarle y no conocer si cliente tiene una cartera o si tiene dinero. Esto es lo que a mi modo de entender, trata la Ley de Demeter. La delegación del comportamiento de clases, es una mejor manera de resolver este problema y recuerda que delegator está también para ayudarnos.

2 comentarios en “Un modelo sólo debe hablar con su asociación inmediata – Law of Demeter con Rails

  1. Ricardo dijo:

    Muy bueno! Es cierto que muchas veces aplicar la ley de Demeter puede ser bastante restrictivo además de llevarte más tiempo de desarrollo, pero no cabe duda que luego el resultado final merece la pena.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s