Rails: Loi de la confusion de Demeter

13

Je lis un livre intitulé Rails AntiPatterns et ils parlent d'utiliser la délégation pour éviter d'enfreindre la loi de Demeter. Voici leur premier exemple:

Ils croient qu'appeler quelque chose comme ça dans le contrôleur est mauvais (et je suis d'accord)

@street = @invoice.customer.address.street

La solution proposée consiste à procéder comme suit:

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

Ils affirment que puisque vous n'utilisez qu'un seul point, vous n'enfreignez pas la loi de Déméter ici. Je pense que c'est incorrect, car vous passez toujours par le client pour passer par l'adresse pour obtenir la rue de la facture. J'ai principalement eu cette idée dans un article de blog que j'ai lu:

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

Dans le billet de blog, le premier exemple est

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

Le blog indique que bien qu'il n'y ait qu'un seul point customer.cashau lieu de customer.wallet.cash, ce code viole toujours la loi de Demeter.

Maintenant, dans la méthode Paperboy collect_money, nous n'avons pas deux points, nous en avons juste un dans "customer.cash". Cette délégation a-t-elle résolu notre problème? Pas du tout. Si nous examinons le comportement, un paperboy atteint toujours directement le portefeuille d'un client pour retirer de l'argent.

ÉDITER

Je comprends parfaitement et je suis d'accord que c'est toujours une violation et je dois créer une méthode Walletappelée appelée qui gère le paiement pour moi et que je devrais appeler cette méthode à l'intérieur de la Customerclasse. Ce que je ne comprends pas, c'est que selon ce processus, mon premier exemple viole toujours la loi de Déméter, car il cherche Invoicetoujours directement Customerà obtenir la rue.

Quelqu'un peut-il m'aider à dissiper la confusion? Je cherchais depuis 2 jours à essayer de laisser ce sujet pénétrer, mais c'est toujours déroutant.

user2158382
la source
2
question similaire ici
thorsten müller
Je ne pense pas que le 2ème exemple (le paperboy) du blog viole la loi de Demeter. Cela peut être une mauvaise conception (vous supposez que le client paiera en espèces), mais ce n'est PAS une violation de la loi de Demeter. Toutes les erreurs de conception ne sont pas causées par la violation de cette loi. L'auteur est confus à l'OMI.
Andres F.
1
Veuillez ne pas poster la même question sur plusieurs sites .
Gilles 'SO- arrête d'être méchant'

Réponses:

24

Votre premier exemple ne viole pas la loi de Demeter. Oui, avec le code tel qu'il est, disant @invoice.customer_streetqu'il arrive qu'il obtienne la même valeur qu'une hypothétique @invoice.customer.address.street, mais à chaque étape de la traversée, la valeur renvoyée est décidée par l'objet demandé - ce n'est pas que "le paperboy atteint le portefeuille du client ", c'est que" le paperboy demande au client de l'argent, et le client arrive à obtenir l'argent de son portefeuille ".

Quand vous dites @invoice.customer.address.street, vous assumez la connaissance des clients et adresse internes - c'est la mauvaise chose. Quand vous dites , vous demandez au "hé, j'aimerais la rue du client, vous décidez comment vous l'obtenez ". Le client dit alors à son adresse: "hé j'aimerais votre rue, vous décidez comment vous l'obtenez ".@invoice.customer_streetinvoice

L'idée maîtresse de Demeter n'est pas "vous ne pouvez jamais connaître les valeurs d'objets éloignés dans le graphique de vous"; c'est plutôt "vous- même ne devez pas parcourir loin le long du graphique d'objet afin d'obtenir des valeurs".

Je suis d'accord que cela peut sembler être une distinction subtile, mais considérez ceci: dans le code conforme à Demeter, combien de code doit changer lorsque la représentation interne d'un addresschange? Qu'en est-il du code non conforme à Demeter?

AakashM
la source
C'est exactement le genre d'explication que je cherchais! Je vous remercie.
user2158382
Très bonne explication. J'ai une question: 1) Si l'objet facture veut retourner un objet client au client de la facture, cela ne signifie pas nécessairement que c'est le même objet client qu'il détient en interne. Il peut simplement s'agir d'un objet créé, à la volée, dans le but de retourner au client un joli ensemble de données packagé avec plusieurs valeurs. En utilisant la logique que vous présentez, vous dites que la facture ne peut pas avoir un champ qui représente plus d'une donnée. Ou est-ce que je manque quelque chose.
zumalifeguard
2

Le premier exemple et le second ne sont en fait pas très identiques. Alors que le premier parle de règles générales de "un point", le second parle davantage d'autres choses dans la conception OO, en particulier " Tell, Don't ask "

La délégation est une technique efficace pour éviter les violations de la loi de Demeter, mais uniquement pour le comportement, pas pour les attributs. - Du deuxième exemple, le blog de Dan

Encore une fois, " uniquement pour le comportement, pas pour les attributs "

Si vous demandez des attributs, vous êtes censé le demander . "Hé, mec, combien d'argent as-tu en poche? Montre-moi, je vais évaluer si tu peux payer ça." C'est faux, aucun commerçant ne se comportera comme ça. Au lieu de cela, ils diront: "Veuillez payer"

customer.pay(due_amount)

Ce sera le devoir du client d'évaluer s'il doit payer et s'il peut payer. Et la tâche du commis est terminée après avoir dit au client de payer.

Alors, le deuxième exemple prouve-t-il que le premier est faux?

À mon avis. Non , tant que:

1. Vous le faites avec auto-contrainte.

Bien que vous puissiez accéder à tous les attributs du client @invoicepar délégation, vous en avez rarement besoin dans les cas normaux.

Pensez à une page affichant une facture dans une application Rails. Il y aura une section en haut pour montrer les détails du client. Donc, dans le modèle de facture, allez-vous coder comme ça?

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

C'est faux et inefficace. Une meilleure approche est

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

Ensuite, laissez le client partiel pour traiter tous les attributs appartient au client.

Donc, en général, vous n'en avez pas besoin. Mais vous pouvez avoir une page de liste affichant toutes les factures récentes, il y a un champ d'information dans chacun liaffichant le nom du client. Dans ce cas, vous devez afficher l'attribut du client et il est tout à fait légitime de coder le modèle comme

= @invoice.customer_name

2. Aucune action supplémentaire ne dépend de cet appel de méthode.

Dans le cas ci-dessus de la page de liste, la facture a demandé l'attribut de nom du client, mais son but réel est de " montrer mon nom ", donc c'est fondamentalement toujours un comportement mais pas d' attribut . Il n'y a aucune autre évaluation et action basée sur cet attribut comme, si votre nom est "Mike", je vous aimerai et je vous donnerai 30 jours de crédit supplémentaire. Non, facturez simplement "montrez-moi votre nom", pas plus. C'est donc tout à fait acceptable selon la règle "Ne dites pas" de l'exemple 2.

Billy Chan
la source
0

Lisez la suite dans le deuxième article et je pense que l'idée deviendra plus claire. L'idée que le client offre simplement la possibilité de payer et de cacher complètement où le boîtier est conservé. Est-ce un champ, un membre d'un portefeuille ou autre chose? L'appelant ne sait pas, n'a pas besoin de savoir et ne change pas si ce détail d'implémentation change.

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

Je pense donc que votre deuxième référence donne une recommandation plus utile.

L'idée du «point unique» est un succès partiel, en ce sens qu'elle cache des détails profonds, mais accroît toujours le couplage entre les composants séparés.

djna
la source
Désolé peut-être que je n'étais pas clair, mais je comprends parfaitement le deuxième exemple et je comprends que vous devez faire l'abstraction que vous avez publiée, mais ce que je ne comprends pas est mon premier exemple. Selon l'article de blog, mon premier exemple est incorrect
user2158382
0

On dirait que Dan a dérivé son exemple de cet article: The Paperboy, The Wallet et The Law Of Demeter

Loi de Déméter Une méthode d'un objet ne doit invoquer que les méthodes des types d'objets suivants:

  1. lui-même
  2. ses paramètres
  3. tous les objets qu'il crée / instancie
  4. ses objets composants directs

Quand et comment appliquer la loi de Demeter

Alors maintenant, vous avez une bonne compréhension de la loi et de ses avantages, mais nous n'avons pas encore discuté de la façon d'identifier les endroits du code existant où nous pouvons l'appliquer (et tout aussi important, où NE PAS l'appliquer ...)

  1. Déclarations enchaînées `` get '' - Le premier endroit le plus évident pour appliquer la loi de Demeter est les lieux de code qui ont des get() déclarations répétées

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

    comme si lorsque notre canonique pour cet exemple était arrêté par le flic, nous pourrions voir:

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

  2. beaucoup d'objets «temporaires» - L'exemple de licence ci-dessus ne serait pas mieux si le code ressemblait,

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

    c'est équivalent, mais plus difficile à détecter.

  3. Importation de nombreuses classes - Sur le projet Java sur lequel je travaille, nous avons une règle selon laquelle nous importons uniquement les classes que nous utilisons réellement; vous ne voyez jamais quelque chose comme

    import java.awt.*;

    dans notre code source. Avec cette règle en place, il n'est pas rare de voir une dizaine d'instructions d'importation provenant toutes du même package. Si cela se produit dans votre code, cela pourrait être un bon endroit pour rechercher des exemples obscurcis de violations. Si vous devez l'importer, vous y êtes couplé. Si cela change, vous devrez peut-être aussi. En important explicitement les classes, vous commencerez à voir à quel point vos classes sont réellement couplées.

Je comprends que votre exemple est en Ruby, mais cela devrait s'appliquer à toutes les langues POO.

M. Polywhirl
la source