Rotaie: Legge della confusione di Demetra


13

Sto leggendo un libro intitolato Rails AntiPatterns e parlano dell'utilizzo della delega per evitare di infrangere la Legge di Demetra. Ecco il loro primo esempio:

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

@street = @invoice.customer.address.street

La soluzione proposta è quella di effettuare le seguenti operazioni:

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 dato 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 cercare l'indirizzo per ottenere la via della fattura. Ho avuto questa idea principalmente da un post sul blog che ho letto:

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

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 sul blog afferma che sebbene ci sia solo un punto customer.cashinvece di customer.wallet.cash, questo codice viola ancora la Legge di Demetra.

Ora nel metodo paperboy collect_money, non abbiamo due punti, ne abbiamo solo uno in "customer.cash". Questa delegazione ha risolto il nostro problema? Affatto. Se osserviamo il comportamento, un ragazzo di carta sta ancora raggiungendo direttamente il portafoglio di un cliente per ottenere incassi.

MODIFICARE

Capisco perfettamente e accetto che questa è ancora una violazione e ho bisogno di creare un metodo Walletchiamato prelievo che gestisca il pagamento per me e che dovrei chiamare quel metodo all'interno della Customerclasse. Quello che non capisco è che secondo questo processo, il mio primo esempio viola ancora la Legge di Demetra perché Invoicesta ancora raggiungendo direttamente Customerla strada.

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


2
domanda simile qui
Thorsten Müller,

Non credo che il secondo esempio (il ragazzo dei giornali) del blog violi la Legge di Demetra. Potrebbe essere una cattiva progettazione (si presume che il cliente pagherà in contanti), ma NON si tratta di una violazione della legge di Demeter. Non tutti gli errori di progettazione sono causati dalla violazione di questa legge. L'autore è confuso IMO.
Andres F.

Risposte:


24

Il tuo primo esempio non viola la legge di Demetra. Sì, con il codice così com'è, dicendo @invoice.customer_streetcapita di ottenere lo stesso valore di un ipotetico @invoice.customer.address.street, ma ad ogni passaggio del traversal, il valore restituito viene deciso dall'oggetto che viene chiesto - non è che "il paperboy raggiunga il portafoglio del cliente ", è che" il paperboy chiede al cliente contanti e il cliente riceve il denaro dal proprio portafoglio ".

Quando dici @invoice.customer.address.street, stai assumendo la conoscenza del cliente e rivolgiti agli interni - questa è la cosa brutta. Quando dici @invoice.customer_street, stai chiedendo invoice"ehi, mi piacerebbe la strada del cliente, decidi tu come ottenerlo ". Il cliente quindi dice al suo indirizzo "hey mi piacerebbe la tua strada, decidi tu come ottenerla ".

La spinta di Demetra non è "non puoi mai conoscere i valori degli oggetti lontani nel grafico da te"; è invece "tu stesso che non devi attraversare molto lungo il grafico degli oggetti per ottenere valori".

Sono d'accordo che ciò possa sembrare una sottile distinzione, ma considera questo: nel codice conforme a Demeter, quanto codice deve cambiare quando cambia la rappresentazione interna di un address? Che dire del codice non conforme a Demeter?


Questo è esattamente il tipo di spiegazione che stavo cercando! Grazie.
user2158382,

Ottima spiegazione Ho una domanda: 1) Se l'oggetto fattura vuole restituire un oggetto cliente al cliente della fattura, ciò non significa necessariamente che sia lo stesso oggetto cliente che contiene internamente. Può essere semplicemente un oggetto creato, al volo, allo scopo di restituire al client un set di dati ben confezionato con più valori al suo interno. Usando la logica che presenti, stai dicendo che la fattura non può avere un campo che rappresenta più di un dato. Oppure mi sfugge qualcosa.
zumalifeguard,

2

Il primo esempio e il secondo non sono in realtà molto simili. Mentre il primo parla delle regole generali di "un punto", il secondo parla di più di altre cose nel design di OO, in particolare " Tell, Don't ask "

La delega è una tecnica efficace per evitare violazioni della Legge di Demetra, ma solo per comportamento, non per attributi. - Dal secondo esempio, il blog di Dan

Ancora una volta, " solo per comportamento, non per attributi "

Se chiedi attributi, dovresti chiedere . "Ehi, ragazzo, quanti soldi hai in tasca? Dimostrami, valuterò se riesci a pagare." È sbagliato, nessun commesso si comporterà in questo modo. Invece, diranno "Per favore paga"

customer.pay(due_amount)

Sarà compito del cliente valutare se deve pagare e se può pagare. E il compito dell'impiegato è finito dopo aver detto al cliente di pagare.

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

Secondo me. No , purché:

1. Lo fai con autocontrollo.

Sebbene sia possibile accedere a tutti gli attributi del cliente @invoicetramite 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
  ....

È sbagliato e inefficiente. Un approccio migliore è

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

Quindi consentire al cliente di elaborare parzialmente tutti gli attributi che appartengono al cliente.

Quindi generalmente non ne hai bisogno. Ma potresti avere una pagina di elenco che mostra tutte le fatture recenti, in ogni campo è presente un briefing licon 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 chiedeva l'attributo del nome del cliente, ma il suo vero scopo è " mostrami il tuo nome ", quindi in pratica è ancora un comportamento ma non un attributo . Non ci sono ulteriori valutazioni e azioni basate su questo attributo come, se il tuo nome è "Mike", mi piacerai e ti darò credito per 30 giorni in più. No, la fattura dice solo "mostrami il tuo nome", non di più. Quindi è del tutto accettabile secondo la regola "Tell Don't Ask" nell'esempio 2.


0

Leggi più avanti nel secondo articolo e penso che l'idea diventerà più chiara. L'idea è solo quella di offrire ai clienti la possibilità di pagare e nascondere completamente dove viene conservato il caso. È un campo, un membro di un portafoglio o qualcos'altro? Il chiamante non lo sa, non ha bisogno di sapere e non cambia se i dettagli dell'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 stia dando una raccomandazione più utile.

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


Scusa forse non ero chiaro, ma capisco perfettamente il secondo esempio e capisco che devi fare l'astrazione che hai pubblicato, ma quello che non capisco è il mio primo esempio. Secondo il post del blog, il mio primo esempio non è corretto
user2158382

0

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

Legge di Demetra Un metodo di un oggetto dovrebbe invocare solo i metodi dei seguenti tipi di oggetti:

  1. si
  2. i suoi parametri
  3. qualsiasi oggetto che crea / crea istanze
  4. i suoi oggetti componenti diretti

Quando e come applicare la legge di Demetra

Quindi ora hai una buona conoscenza della legge e dei suoi benefici, ma non abbiamo ancora discusso su come identificare i luoghi nel codice esistente in cui possiamo applicarlo (e altrettanto importante, dove NON applicarlo ...)

  1. Dichiarazioni "get" concatenate : il primo, il punto più ovvio in cui applicare la Legge di Demetra sono i luoghi di codice che hanno ripetute get() dichiarazioni,

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

    come se quando la nostra persona canonica per questo esempio fosse stata fermata dal poliziotto, potremmo vedere:

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

  2. molti oggetti "temporanei" . L'esempio di licenza sopra riportato non sarebbe migliore se il codice fosse simile,

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

    è equivalente, ma più difficile da rilevare.

  3. Importazione di molte classi - Nel progetto Java su cui lavoro, abbiamo una regola che importiamo solo le classi che effettivamente utilizziamo; non vedi mai qualcosa del genere

    import java.awt.*;

    nel nostro codice sorgente. Con questa regola in atto, non è raro vedere una dozzina di istruzioni di importazione che provengono tutte dallo stesso pacchetto. Se ciò accade nel tuo codice, potrebbe essere un buon posto per cercare esempi oscuri di violazioni. Se devi importarlo, sei accoppiato ad esso. Se cambia, potrebbe essere necessario. Importando esplicitamente le classi, inizierai a vedere quanto sono realmente accoppiate le tue classi.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.