Rails: Law of Demeter Confusion

13

Sto leggendo un libro chiamato Rails AntiPatterns e parlano di usare la delega per evitare di infrangere la legge di Demeter. Ecco il loro primo esempio:

Credono che chiamare qualcosa di simile nel controller sia sbagliato (e sono d'accordo)

@street = @invoice.customer.address.street

La loro soluzione proposta è di fare quanto segue:

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

Stanno affermando che dal momento che usi solo un punto, non stai infrangendo la Legge di Demetra qui. Penso che questo non sia corretto, perché stai ancora passando attraverso il cliente per passare attraverso l'indirizzo per ottenere la via della fattura. Principalmente ho avuto questa idea da un post sul blog che ho letto:

link

Nel post del blog l'esempio principale è

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

Il post del blog afferma che, sebbene esista solo un punto customer.cash invece di customer.wallet.cash , questo codice viola ancora la legge di Demeter.

Now in the Paperboy collect_money method, we don't have two dots, we just have one in "customer.cash". Has this delegation solved our problem? Not at all. If we look at the behavior, a paperboy is still reaching directly into a customer's wallet to get cash out.

Modifica

Comprendo e accetto completamente che si tratta ancora di una violazione e ho bisogno di creare un metodo in Wallet chiamato withdraw che gestisce il pagamento per me e che dovrei chiamare tale metodo all'interno della classe Customer . Quello che non capisco è che, secondo questo processo, il mio primo esempio viola ancora la legge di Demeter perché Invoice sta ancora raggiungendo direttamente in Customer per ottenere la strada.

Qualcuno può aiutarmi a chiarire la confusione. Ho cercato gli ultimi 2 giorni cercando di far affondare questo argomento, ma è ancora confuso.

    
posta user2158382 17.10.2013 - 10:47
fonte

4 risposte

24

Il tuo primo esempio non viola la legge di Demeter. Sì, con il codice così com'è, dicendo @invoice.customer_street capita di ottenere lo stesso valore che sarebbe un ipotetico @invoice.customer.address.street , ma ad ogni passo del traversale, il valore restituito è deciso dall'oggetto che viene chiesto - non è che "il fattorino raggiunge il portafoglio del cliente", è che "il fattorino chiede al cliente denaro contante, e il cliente capita di ottenere il denaro dal proprio portafoglio ".

Quando dici @invoice.customer.address.street , stai presupponendo la conoscenza degli interni dei clienti e degli indirizzi: questo è la cosa negativa. Quando dici @invoice.customer_street , stai chiedendo il invoice , "hey, mi piacerebbe la strada del cliente, tu decidi come ottenerlo ". Il cliente quindi dice al suo indirizzo, "hey, mi piacerebbe la tua strada, tu decidi come ottenerlo ".

La spinta di Demetra è non 'non puoi mai sapere valori da oggetti lontani nel grafico da te "; è invece' tu te stesso non deve attraversare lontano lungo il grafico dell'oggetto per ottenere valori '.

Sono d'accordo che questo può sembrare una sottile distinzione, ma considera questo: nel codice compatibile con Demetra, quanto deve cambiare il codice quando cambia la rappresentazione interna di address ? Che ne dici di un codice non conforme a Demeter?

    
risposta data 17.10.2013 - 11:13
fonte
2

Il primo esempio e il secondo non sono in realtà molto simili. Mentre il primo parla di regole generali di "un punto", il secondo parla di altre cose nel design OO, in particolare " Dillo, Non chiedere "

Delegation is an effective technique to avoid Law of Demeter violations, but only for behavior, not for attributes. -- From the second example, Dan's blog

Anche in questo caso, " solo per comportamento, non per attributi "

Se chiedi attributi, devi chiedere . "Ehi, ragazzo, quanti soldi hai in tasca? Fammi vedere, valuterò se puoi pagare questo." È sbagliato, nessun commesso commerciale si comporterà in questo modo. Invece, diranno "Per favore paga"

customer.pay(due_amount)

Sarà compito del cliente valutare se debba pagare e se può pagare. E il compito del cancelliere è finito dopo aver detto al cliente di pagare.

Quindi, il secondo esempio dimostra che il primo è sbagliato?

Secondo me. No , a condizione che:

1. Lo fai con self-constraint.

Sebbene sia possibile accedere a tutti gli attributi del cliente in @invoice per delega, raramente è necessario in casi normali.

Pensa a una pagina che mostra una fattura in un'app Rails. Ci sarà una sezione in alto per mostrare i dettagli del cliente. Quindi, nel modello di fattura, codificherai in questo modo?

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

Questo è sbagliato e inefficiente. Un approccio migliore è

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

Quindi lascia che il cliente parta per elaborare tutti gli attributi appartiene al cliente.

Quindi generalmente non ne hai bisogno. Ma potresti avere una pagina di elenco che mostra tutte le fatture recenti, c'è un campo di briefing in ogni li che mostra il nome del cliente. In questo caso, è necessario mostrare l'attributo del cliente ed è del tutto legittimo codificare il modello come

= @invoice.customer_name

2. Non ci sono ulteriori azioni a seconda di questa chiamata al metodo.

Nel caso precedente della pagina di elenco, la fattura ha chiesto l'attributo del nome del cliente, ma il suo vero scopo è " mostrami il tuo nome ", quindi è fondamentalmente ancora un comportamento ma non attributo . Non ci sono ulteriori valutazioni e azioni basate su questo attributo, come, se il tuo nome è "Mike", mi piacerà e ti darò 30 giorni in più di credito. No, la fattura dice semplicemente "mostrami il tuo nome", non di più. Quindi è totalmente accettabile secondo la regola "Tell Do not Ask" nell'esempio 2.

    
risposta data 17.10.2013 - 11:13
fonte
0

Leggi oltre nel secondo articolo e penso che l'idea diverrà più chiara. L'idea è solo quella di offrire ai clienti la possibilità di pagare e di nascondere completamente dove viene custodito il caso. È un campo, un membro di un portafoglio o qualcos'altro? Il chiamante non sa, non ha bisogno di sapere e non cambia se tali dettagli di implementazione cambiano.

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

Quindi penso che il tuo secondo riferimento fornisca una raccomandazione più utile.

L'idea unica di "un punto" è un successo parziale, in quanto nasconde alcuni dettagli profondi, ma continua ad aumentare l'accoppiamento tra componenti separati.

    
risposta data 17.10.2013 - 10:54
fonte
0

Sembra che Dan abbia tratto il suo esempio da questo articolo: The Paperboy, The Wallet, e The Law Of Demeter

Law Of Demeter A method of an object should invoke only the methods of the following kinds of objects:

  1. itself
  2. its parameters
  3. any objects it creates/instantiates
  4. its direct component objects

When and How to Apply The Law Of Demeter

So now you have a good understanding of the law and it's benefits, but we haven't yet discussed how to identify places in existing code where we can apply it (and just as important, where NOT to apply it...)

  1. Chained 'get' Statements - The first, most obvious place to apply the Law Of Demeter is places of code that have repeated get() statements,

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

    as if when our canonical person for this example were pulled over by the cop, we might see:

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

  2. lots of 'temporary' objects - The above license example would be no better if the code looked like,

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

    it is equivalent, but harder to detect.

  3. Importing Many Classes - On the Java project I work on, we have a rule that we only import classes we actually use; you never see something like

    import java.awt.*;

    in our source code. With this rule in place, it is not uncommon to see a dozen or so import statements all coming from the same package. If this is happening in your code, it could be a good place to look for obscured examples of violations. If you need to import it, you are coupled to it. If it changes, you may have to as well. By explicitly importing the classes, you will begin to see how coupled your classes really are.

Capisco che il tuo esempio sia in Ruby, ma questo dovrebbe applicarsi a tutte le lingue OOP.

    
risposta data 17.10.2013 - 13:00
fonte

Leggi altre domande sui tag