Rieles: Ley de confusión de Demeter

13

Estoy leyendo un libro llamado Rails AntiPatterns y hablan sobre el uso de la delegación para evitar infringir la Ley de Demeter. Aquí está su primer ejemplo:

Creen que llamar a algo así en el controlador es malo (y estoy de acuerdo)

@street = @invoice.customer.address.street

Su solución propuesta es hacer lo siguiente:

class Customer

    has_one :address
    belongs_to :invoice

    def street
        address.street
    end
end

class Invoice

    has_one :customer

    def customer_street
        customer.street
    end
end

@street = @invoice.customer_street

Afirman que dado que solo usa un punto, no está violando la Ley de Deméter aquí. Creo que esto es incorrecto, porque todavía está pasando por el cliente para ir a través de la dirección para obtener la calle de la factura. Principalmente obtuve esta idea de una publicación de blog que leí:

http://www.dan-manges.com/blog/37

En la publicación del blog, el mejor ejemplo es

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  # attribute delegation
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collect_money(customer, due_amount)
    if customer.cash < due_ammount
      raise InsufficientFundsError
    else
      customer.cash -= due_amount
      @collected_amount += due_amount
    end
  end
end

La publicación del blog indica que aunque solo hay un punto en customer.cashlugar de customer.wallet.casheste, este código aún viola la Ley de Demeter.

Ahora en el método Paperboy collect_money, no tenemos dos puntos, solo tenemos uno en "customer.cash". ¿Esta delegación ha resuelto nuestro problema? De ningún modo. Si observamos el comportamiento, un repartidor de periódicos todavía está buscando directamente en la billetera de un cliente para sacar dinero.

EDITAR

Entiendo completamente y acepto que esto sigue siendo una violación y que necesito crear un método Walletllamado retiro que maneja el pago por mí y que debería llamar a ese método dentro de la Customerclase. Lo que no entiendo es que, de acuerdo con este proceso, mi primer ejemplo todavía viola la Ley de Deméter porque Invoicetodavía está llegando directamente Customera la calle.

¿Alguien puede ayudarme a aclarar la confusión? He estado buscando durante los últimos 2 días tratando de dejar que este tema se hunda, pero aún es confuso.

usuario2158382
fuente
2
pregunta similar aquí
thorsten müller
No creo que el segundo ejemplo (el repartidor de periódicos) del blog viole la Ley de Deméter. Puede ser un mal diseño (está asumiendo que el cliente pagará en efectivo), pero eso NO es una violación de la Ley de Demeter. No todos los errores de diseño son causados ​​por infringir esta Ley. El autor está confundido OMI.
Andres F.
1
No publique la misma pregunta en varios sitios .
Gilles 'SO- deja de ser malvado'

Respuestas:

24

Su primer ejemplo no viola la ley de Demeter. Sí, con el código tal como está, decir @invoice.customer_streetque sucede que tiene el mismo valor que un hipotético @invoice.customer.address.street, pero en cada paso de la travesía, el valor devuelto es decidido por el objeto al que se le pregunta : no es que "el repartidor llegue al billetera del cliente ", es que" el repartidor de periódicos le pide efectivo al cliente, y el cliente obtiene el efectivo de su billetera ".

Cuando dices @invoice.customer.address.street, estás asumiendo el conocimiento de clientes y direcciones internas: esto es lo malo. Cuando dices @invoice.customer_street, estás preguntando invoice, "oye, me gustaría la calle del cliente, tú decides cómo la obtienes ". El cliente luego dice a su dirección, "oye, me gustaría tu calle, tú decides cómo la obtienes ".

El objetivo de Demeter no es "nunca se pueden conocer los valores de los objetos lejanos en el gráfico", sino que "usted mismo no debe atravesar el gráfico del objeto para obtener valores".

Estoy de acuerdo en que esto puede parecer una distinción sutil, pero considere esto: en el código compatible con Demeter, ¿cuánto código debe cambiar cuando cambia la representación interna de un address? ¿Qué pasa con el código no compatible con Demeter?

AakashM
fuente
¡Este es exactamente el tipo de explicación que estaba buscando! Gracias.
user2158382
Muy buena explicación. Tengo una pregunta: 1) Si el objeto de la factura quiere devolver un objeto de cliente al cliente de la factura, eso no significa necesariamente que sea el mismo objeto de cliente que tiene internamente. Puede ser simplemente un objeto creado, sobre la marcha, con el fin de devolver al cliente un buen conjunto de datos empaquetados con múltiples valores. Usando la lógica que presenta, está diciendo que la factura no puede tener un campo que represente más de un dato. O me estoy perdiendo algo.
zumalifeguard
2

El primer ejemplo y el segundo en realidad no son muy iguales. Mientras que el primero habla sobre las reglas generales de "un punto", el segundo habla más sobre otras cosas en el diseño OO, especialmente " Dile, no preguntes "

La delegación es una técnica efectiva para evitar violaciones de la Ley de Demeter, pero solo por comportamiento, no por atributos. - Del segundo ejemplo, el blog de Dan

Nuevamente, " solo por comportamiento, no por atributos "

Si pide atributos, se supone que debe preguntar . "Oye, ¿cuánto dinero tienes en el bolsillo? Muéstrame, evaluaré si puedes pagar esto". Eso está mal, ningún empleado de compras se comportará así. En cambio, dirán: "Por favor pague"

customer.pay(due_amount)

Será deber del cliente evaluar si debe pagar y si puede pagar. Y la tarea del secretario termina después de decirle al cliente que pague.

Entonces, ¿el segundo ejemplo prueba que el primero está mal?

En mi opinión. No , siempre que:

1. Lo haces con autolimitación.

Si bien puede acceder a todos los atributos del cliente @invoicepor delegación, rara vez lo necesita en casos normales.

Piense en una página que muestra una factura en una aplicación Rails. Habrá una sección en la parte superior para mostrar los detalles del cliente. Entonces, en la plantilla de la factura, ¿codificará así?

#customer-info
  = @invoice.customer_name
  = @invoice.customer_address
  ....

Eso está mal e ineficiente. Un mejor enfoque es

#customer-info
  = render partial: 'invoice_header_customer', 
           locals: {customer: @invoice.customer}

Luego, deje que el cliente procese todos los atributos que pertenecen al cliente.

Así que generalmente no necesitas eso. Pero puede tener una página de lista que muestre todas las facturas recientes, hay un campo de información en cada una que limuestra el nombre del cliente. En este caso, necesita que se muestre el atributo del cliente, y es totalmente legítimo codificar la plantilla como

= @invoice.customer_name

2. No hay más acciones dependiendo de esta llamada al método.

En el caso anterior de la página de lista, la factura preguntaba el atributo del nombre del cliente, pero su propósito real es " muéstrame tu nombre ", por lo que básicamente sigue siendo un comportamiento pero no un atributo . No hay más evaluaciones y acciones basadas en este atributo, por ejemplo, si tu nombre es "Mike", me agradarás y te daré 30 días más de crédito. No, la factura solo dice "muéstrame tu nombre", no más. Entonces, eso es totalmente aceptable de acuerdo con la regla "Dile, no preguntes" en el ejemplo 2.

Billy Chan
fuente
0

Lea más en el segundo artículo y creo que la idea será más clara. La idea es simplemente que el cliente ofrezca la capacidad de pagar y esconderse completamente donde se guarda el caso. ¿Es un campo, un miembro de una billetera o algo más? La persona que llama no lo sabe, no necesita saberlo y no cambia si ese detalle de implementación cambia.

class Wallet
  attr_accessor :cash
  def withdraw(amount)
     raise InsufficientFundsError if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet
  # behavior delegation
  def pay(amount)
    @wallet.withdraw(amount)
  end
end
class Paperboy
  def collect_money(customer, due_amount)
    @collected_amount += customer.pay(due_amount)
  end
end

Así que creo que su segunda referencia está dando una recomendación más útil.

La idea única de "un punto" es un éxito parcial, ya que oculta algunos detalles profundos, pero aún así aumenta el acoplamiento entre componentes separados.

djna
fuente
Lo siento, tal vez no estaba claro, pero entiendo perfectamente el segundo ejemplo y entiendo que necesitas hacer la abstracción que publicaste, pero lo que no entiendo es mi primer ejemplo. Según la publicación del blog, mi primer ejemplo es incorrecto
usuario2158382
0

Suena como que Dan obtuvo su ejemplo de este artículo: The Paperboy, The Wallet y The Law Of Demeter

Ley de Demeter Un método de un objeto debe invocar solo los métodos de los siguientes tipos de objetos:

  1. sí mismo
  2. sus parámetros
  3. cualquier objeto que crea / crea instancias
  4. sus objetos componentes directos

Cuándo y cómo aplicar la ley de Demeter

Entonces, ahora tiene una buena comprensión de la ley y sus beneficios, pero aún no hemos discutido cómo identificar los lugares en el código existente donde podemos aplicarlo (e igual de importante, dónde NO aplicarlo ...)

  1. Declaraciones encadenadas de 'obtener' : el primer lugar más obvio para aplicar la Ley de Demeter son los lugares de código que tienen get() declaraciones repetidas ,

    value = object.getX().getY().getTheValue();

    como si el policía detuviera a nuestra persona canónica para este ejemplo, podríamos ver:

    license = person.getWallet().getDriversLicense();

  2. muchos objetos 'temporales' : el ejemplo de la licencia anterior no sería mejor si el código fuera así,

    Wallet tempWallet = person.getWallet(); license = tempWallet.getDriversLicense();

    Es equivalente, pero más difícil de detectar.

  3. Importar muchas clases : en el proyecto Java en el que trabajo, tenemos una regla que solo importamos las clases que realmente usamos; nunca ves algo como

    import java.awt.*;

    en nuestro código fuente Con esta regla, no es raro ver una docena de declaraciones de importación, todas provenientes del mismo paquete. Si esto sucede en su código, podría ser un buen lugar para buscar ejemplos ocultos de violaciones. Si necesita importarlo, está acoplado a él. Si cambia, es posible que también deba hacerlo. Al importar explícitamente las clases, comenzará a ver qué tan acopladas están realmente sus clases.

Entiendo que su ejemplo está en Ruby, pero esto debería aplicarse en todos los idiomas de OOP.

Mr. Polywhirl
fuente