Rails: Gesetz der Demeter-Verwirrung


13

Ich lese ein Buch mit dem Titel Rails AntiPatterns und sie sprechen über die Verwendung von Delegationen, um das Gesetz von Demeter nicht zu brechen. Hier ist ihr Paradebeispiel:

Sie glauben, dass es schlecht ist, so etwas im Controller aufzurufen (und ich stimme zu)

@street = @invoice.customer.address.street

Ihre vorgeschlagene Lösung besteht darin, Folgendes zu tun:

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

Sie behaupten, dass Sie hier nicht gegen das Gesetz von Demeter verstoßen, da Sie nur einen Punkt verwenden. Ich denke, das ist falsch, weil Sie immer noch durch Kunden gehen, um die Adresse durchzugehen, um die Straße der Rechnung zu erhalten. Diese Idee kam mir vor allem in einem Blogbeitrag, den ich las:

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

Im Blogpost ist das Paradebeispiel

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

Der Blogbeitrag besagt, dass dieser Code , obwohl es nur einen Punkt customer.cashstattdessen gibt customer.wallet.cash, immer noch gegen das Gesetz von Demeter verstößt.

Jetzt haben wir in der Paperboy-Methode collect_money nicht zwei Punkte, sondern nur einen in "customer.cash". Hat diese Delegation unser Problem gelöst? Überhaupt nicht. Wenn wir uns das Verhalten ansehen, greift ein Zeitungsjunge immer noch direkt in die Brieftasche eines Kunden, um Bargeld herauszuholen.

BEARBEITEN

Ich verstehe und bin damit einverstanden, dass dies immer noch ein Verstoß ist und ich muss eine Methode Walletnamens "Zurückziehen" erstellen , die die Zahlung für mich abwickelt und die ich innerhalb der CustomerKlasse aufrufen sollte . Was ich nicht verstehe, ist, dass nach diesem Verfahren mein erstes Beispiel immer noch gegen das Gesetz von Demeter verstößt, weil Invoicees immer noch direkt hineinreicht Customer, um auf die Straße zu gelangen.

Kann mir jemand helfen, die Verwirrung zu beseitigen? Ich habe in den letzten zwei Tagen nach Informationen gesucht, um dieses Thema aufzugreifen, aber es ist immer noch verwirrend.


2
ähnliche
frage

Ich glaube nicht, dass das zweite Beispiel (der Paperboy) aus dem Blog gegen das Gesetz von Demeter verstößt. Es könnte schlechtes Design sein (Sie gehen davon aus, dass der Kunde mit Bargeld bezahlt), aber das ist KEIN Verstoß gegen das Demeter-Gesetz. Nicht alle Designfehler werden durch Verstöße gegen dieses Gesetz verursacht. Der Autor ist IMO verwirrt.
Andres F.

Antworten:


24

Ihr erstes Beispiel verstößt nicht gegen das Gesetz von Demeter. Ja, mit dem Code , wie es steht, sagen @invoice.customer_streetgeschieht das Gleiche bekommen Wert , dass eine hypothetische @invoice.customer.address.streetwürde, aber bei jedem Schritt des Traversal, kehrte der Wert durch das Objekt aufgefordert , entschieden wird - es ist nicht , dass „die paperboy greift in die Kundengeldbörse ", so heißt es," der Zeitungsjunge bittet den Kunden um Bargeld, und der Kunde bekommt das Bargeld von seiner Geldbörse ".

Wenn Sie sagen @invoice.customer.address.street, gehen davon aus Sie Kenntnis von Kunden- und Adress Interna - dies die schlechte Sache ist. Wenn Sie sagen @invoice.customer_street, Sie fragen die invoice, "Hey, ich möchte die Straße des Kunden, Sie entscheiden, wie Sie es bekommen ". Der Kunde sagt dann zu seiner Adresse: "Hey, ich möchte deine Straße, du entscheidest, wie du sie bekommst. "

Der Schub von Demeter ist nicht "Sie können niemals Werte von Objekten erkennen, die weit entfernt in der Grafik liegen", sondern "Sie selbst dürfen nicht weit entlang der Objektgrafik wandern, um Werte zu erhalten".

Ich stimme zu, dass dies eine subtile Unterscheidung sein mag, aber bedenken Sie Folgendes: Wie viel Code muss sich in Demeter-konformem Code ändern, wenn sich die interne Darstellung eines Codes addressändert? Was ist mit nicht Demeter-konformem Code?


Das ist genau die Erklärung, nach der ich gesucht habe! Vielen Dank.
user2158382

Sehr gute Erklärung. Ich habe folgende Fragen: 1) Wenn das Rechnungsobjekt ein Kundenobjekt an den Kunden der Rechnung zurückgeben möchte, bedeutet dies nicht unbedingt, dass es dasselbe Kundenobjekt ist, das es intern enthält. Es kann einfach ein Objekt sein, das im laufenden Betrieb erstellt wird, um dem Client einen schön gepackten Datensatz mit mehreren Werten zurückzugeben. Mit der von Ihnen präsentierten Logik sagen Sie, dass die Rechnung kein Feld enthalten darf, das mehr als eine Daten darstellt. Oder vermisse ich etwas?
Zumalifeguard

2

Das erste Beispiel und das zweite sind eigentlich nicht sehr gleich. Während das erste über die allgemeinen Regeln von "one dot" spricht, spricht das zweite mehr über andere Dinge im OO-Design, insbesondere " Tell, Don't ask ".

Die Delegierung ist eine wirksame Technik, um Verstöße gegen das Demeter-Gesetz zu vermeiden, jedoch nur für das Verhalten, nicht für Attribute. - Aus dem zweiten Beispiel, Dans Blog

Wieder „ nur für das Verhalten, nicht für Attribute

Wenn Sie nach Attributen fragen, sollten Sie fragen . "Hey, Mann, wie viel Geld hast du in der Tasche? Zeig mir, ich werde abwägen, ob du das bezahlen kannst." Das ist falsch, kein Einkaufsbeamter wird sich so verhalten. Stattdessen werden sie sagen: "Bitte zahlen"

customer.pay(due_amount)

Es liegt in der Verantwortung des Kunden, zu bewerten, ob er zahlen sollte und ob er zahlen kann. Und die Aufgabe des Angestellten ist erledigt, nachdem der Kunde zum Bezahlen aufgefordert wurde.

Beweist das zweite Beispiel also, dass das erste falsch ist?

Meiner Meinung nach. Nein , solange:

1. Du machst es mit Selbstbeschränkung.

Während Sie auf alle Attribute des Kunden @invoicedelegiert zugreifen können , ist dies im Normalfall selten erforderlich.

Stellen Sie sich eine Seite mit einer Rechnung in einer Rails-App vor. Oben befindet sich ein Abschnitt, in dem die Kundendaten angezeigt werden. Werden Sie in der Rechnungsvorlage so codieren?

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

Das ist falsch und ineffizient. Ein besserer Ansatz ist

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

Lassen Sie dann den Kunden teilweise alle Attribute verarbeiten, die dem Kunden gehören.

Also brauchst du das im Allgemeinen nicht. Möglicherweise haben Sie jedoch eine Listenseite, auf der alle aktuellen Rechnungen aufgeführt sind. In jedem Feld wird lider Name des Kunden angezeigt . In diesem Fall müssen Sie das Attribut des Kunden anzeigen, und es ist absolut legitim, die Vorlage als zu codieren

= @invoice.customer_name

2. Abhängig von diesem Methodenaufruf erfolgt keine weitere Aktion.

Im obigen Fall der Listenseite wurde auf der Rechnung das Namensattribut des Kunden abgefragt, der eigentliche Zweck ist jedoch " Zeigen Sie mir Ihren Namen ". Es handelt sich also im Grunde immer noch um ein Verhalten, aber nicht um ein Attribut . Es gibt keine weitere Bewertung und Aktion basierend auf diesem Attribut wie, wenn Ihr Name "Mike" ist, werde ich Sie mögen und Ihnen 30 Tage mehr Kredit geben. Nein, auf der Rechnung steht nur "Zeig mir deinen Namen", nicht mehr. Das ist also gemäß der Regel "Tell Don't Ask" in Beispiel 2 völlig akzeptabel.


0

Lesen Sie weiter im zweiten Artikel und ich denke, die Idee wird klarer. Die Idee ist nur, dass der Kunde eine Zahlungsmöglichkeit anbietet und vollständig verbirgt, wo der Koffer aufbewahrt wird. Ist es ein Feld, ein Mitglied einer Brieftasche oder etwas anderes? Der Aufrufer weiß es nicht, muss es nicht wissen und ändert sich nicht, wenn sich das Implementierungsdetail ändert.

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

Ich denke, Ihre zweite Referenz gibt eine hilfreichere Empfehlung.

Die "Ein-Punkt" -Idee ist insofern ein Teilerfolg, als sie einige tiefe Details verbirgt, aber die Kopplung zwischen getrennten Komponenten noch verstärkt.


Tut mir leid, vielleicht war mir nicht klar, aber ich verstehe das zweite Beispiel perfekt und ich verstehe, dass Sie die von Ihnen gepostete Abstraktion vornehmen müssen, aber was ich nicht verstehe, ist mein erstes Beispiel. Mein erstes Beispiel ist laut Blog-Post falsch
user2158382 17.10.13

0

Klingt, als hätte Dan sein Beispiel aus diesem Artikel abgeleitet: The Paperboy, The Wallet und The Law Of Demeter

Demeter-Gesetz Eine Methode eines Objekts sollte nur die Methoden der folgenden Arten von Objekten aufrufen:

  1. selbst
  2. seine Parameter
  3. alle Objekte, die es erstellt / instanziiert
  4. seine direkten Komponentenobjekte

Wann und wie das Gesetz von Demeter anzuwenden ist

Nun haben Sie ein gutes Verständnis für das Gesetz und seine Vorteile, aber wir haben noch nicht darüber gesprochen, wie Sie Stellen in vorhandenem Code identifizieren können, an denen wir es anwenden können (und genau so wichtig, wo Sie es NICHT anwenden sollen ...).

  1. Verkettete 'get'-Anweisungen - Der erste, naheliegendste Ort, an dem das Gesetz von Demeter angewendet werden kann, sind Code-Stellen, an denen sich get() Anweisungen wiederholen .

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

    als ob unsere kanonische Person für dieses Beispiel vom Polizisten überfahren worden wäre, könnten wir sehen:

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

  2. viele 'temporäre' Objekte - Das obige Lizenzbeispiel wäre nicht besser, wenn der Code so aussehen würde,

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

    es ist äquivalent, aber schwerer zu erkennen.

  3. Importieren vieler Klassen - In dem Java-Projekt, an dem ich arbeite, gilt die Regel, dass wir nur Klassen importieren, die wir tatsächlich verwenden. so etwas sieht man nie

    import java.awt.*;

    in unserem Quellcode. Mit dieser Regel ist es nicht ungewöhnlich, dass etwa ein Dutzend Importanweisungen aus demselben Paket stammen. Wenn dies in Ihrem Code geschieht, ist dies möglicherweise ein guter Ort, um nach undurchsichtigen Beispielen für Verstöße zu suchen. Wenn Sie es importieren müssen, sind Sie damit verbunden. Wenn es sich ändert, müssen Sie möglicherweise auch. Wenn Sie die Klassen explizit importieren, werden Sie feststellen, wie stark Ihre Klassen tatsächlich gekoppelt sind.

Ich verstehe, dass Ihr Beispiel in Ruby ist, aber dies sollte für alle OOP-Sprachen gelten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.