Spiegazione su come "Tell, Do not Ask" è considerato un buon OO

47

Questo post del blog è stato pubblicato su Hacker News con diversi upvotes. Provenendo dal C ++, la maggior parte di questi esempi sembra andare contro ciò che mi è stato insegnato.

Ad esempio # 2:

Bad:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

contro buono:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Il consiglio in C ++ è che dovresti preferire le funzioni libere invece delle funzioni membro in quanto aumentano l'incapsulamento. Entrambi sono identici semanticamente, quindi perché preferire la scelta che ha accesso a più stati?

Esempio 4:

Bad:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

contro buono:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Perché è responsabilità di User formattare una stringa di errore non correlata? Cosa succede se voglio fare qualcosa oltre a stampare 'No street name on file' se non ha una strada? Cosa succede se la strada è denominata la stessa cosa?

Qualcuno potrebbe illuminarmi sul "Dillo, non chiedere" vantaggi e logica? Non sto cercando il migliore, ma cercando di capire il punto di vista dell'autore.

    
posta Pubby 20.07.2012 - 02:30
fonte

9 risposte

78

Chiedere all'oggetto il suo stato e quindi chiamare i metodi su quell'oggetto in base alle decisioni prese al di fuori dell'oggetto, significa che l'oggetto ora è un'astrazione che perde; alcuni dei suoi comportamenti si trovano all'esterno dell'oggetto, e lo stato interno è esposto (forse inutilmente) al mondo esterno.

You should endeavor to tell objects what you want them to do; do not ask them questions about their state, make a decision, and then tell them what to do.

The problem is that, as the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object. The logic you are implementing is probably the called object’s responsibility, not yours. For you to make decisions outside the object violates its encapsulation.

Sure, you may say, that’s obvious. I’d never write code like that. Still, it’s very easy to get lulled into examining some referenced object and then calling different methods based on the results. But that may not be the best way to go about doing it. Tell the object what you want. Let it figure out how to do it. Think declaratively instead of procedurally!

It is easier to stay out of this trap if you start by designing classes based on their responsibilities; you can then progress naturally to specifying commands that the class may execute, as opposed to queries that inform you as to the state of the object.

link

    
risposta data 20.07.2012 - 02:48
fonte
16

Generalmente, il pezzo suggerisce che non devi esporre lo stato membro perché altri possano ragionare, se puoi ragionarti su di te .

Tuttavia, ciò che non è chiaramente affermato è che questa legge ricade in molto limiti ovvi quando il ragionamento è molto più della responsabilità di una classe specifica. Ad esempio, ogni classe il cui compito è quello di mantenere un certo valore o fornire un valore, in particolare quelli generici o in cui la classe fornisce un comportamento che deve essere esteso.

Ad esempio, se il sistema fornisce temperature come query, domani il client può check_for_underheating senza dover modificare SystemMonitor . Questo non è il caso quando SystemMonitor implementa check_for_overheating stesso. Quindi, una classe SystemMonitor il cui compito è quello di generare un allarme quando la temperatura è troppo alta segue questa, ma una classe SystemMonitor il cui compito è quello di consentire a un altro pezzo di codice di leggere la temperatura in modo che possa controllare, ad esempio , TurboBoost o qualcosa del genere, non dovrebbe.

Si noti inoltre che il secondo esempio utilizza inutilmente l'Anti-pattern dell'oggetto Null.

    
risposta data 20.07.2012 - 03:42
fonte
9

Il vero problema con il tuo esempio di surriscaldamento è che le regole per ciò che si qualifica come surriscaldamento non possono essere facilmente modificate per sistemi diversi. Supponiamo che il sistema A sia come lo hai tu (temp > 100 si sta surriscaldando) ma il sistema B è più delicato (temp > 93 si sta surriscaldando). Modifica la tua funzione di controllo per verificare il tipo di sistema, quindi applica il valore corretto?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

O hai ogni tipo di sistema che definisce la sua capacità di riscaldamento?

Modifica

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

Il primo modo fa diventare brutta la tua funzione di controllo quando inizi a gestire più sistemi. Quest'ultimo consente alla funzione di controllo di essere stabile con il passare del tempo.

    
risposta data 20.07.2012 - 19:16
fonte
6

Prima di tutto, sento di dover fare un'eccezione alla tua caratterizzazione degli esempi come "cattiva" e "buona". L'articolo usa i termini "Non così buono" e "Migliore", penso che quei termini siano stati scelti per una ragione: queste sono linee guida e, a seconda delle circostanze, l'approccio "Non così buono" potrebbe essere appropriato, o addirittura l'unica soluzione.

Quando viene data una scelta, dovresti dare preferenza per includere qualsiasi funzionalità che si basa esclusivamente sulla classe in della classe anziché all'esterno di essa - il motivo è dovuto al incapsulamento e il fatto che rende più facile evolvere la classe nel tempo. La classe fa anche un lavoro migliore di pubblicità delle sue capacità rispetto a un sacco di funzioni gratuite.

A volte devi dirlo, perché la decisione si basa su qualcosa al di fuori della classe o perché è semplicemente qualcosa che non vuoi che la maggior parte degli utenti della classe faccia. A volte lo vuoi dire, perché il comportamento è contro-intuitivo per la classe e non vuoi confondere la maggior parte degli utenti della classe.

Ad esempio, ti lamenti per l'indirizzo che restituisce un messaggio di errore, non lo è, ciò che sta facendo è fornire un valore predefinito. Ma a volte un valore predefinito non è appropriato. Se si tratta di stato o città, è possibile che si desideri un valore predefinito quando si assegna un record a un venditore o a un assegnatario di sondaggi, in modo che tutte le incognite vadano a una persona specifica. D'altra parte, se stavi stampando buste, potresti preferire un'eccezione o una guardia che ti impedisce di sprecare carta su lettere che non possono essere consegnate.

Quindi ci possono essere casi in cui "Non così buono" è la strada da percorrere, ma in generale, "Migliore" è, beh, meglio.

    
risposta data 20.07.2012 - 05:18
fonte
3

Anti-simmetria di dati / oggetto

Come altri hanno sottolineato, Tell-Dont-Ask è specifico per i casi in cui modifica lo stato dell'oggetto dopo che hai chiesto (vedi ad esempio il testo di Pragprog pubblicato altrove su questa pagina). Questo non è sempre il caso, ad es. l'oggetto "utente" non è cambiato dopo che è stato chiesto per il suo user.address. È quindi discutibile se questo è un caso appropriato per applicare Tell-Dont-Ask.

Tell-Dont-Ask si occupa di responsabilità, senza estrarre la logica da una classe che dovrebbe essere giustificata all'interno di essa. Ma non tutta la logica che si occupa degli oggetti è necessariamente la logica di quegli oggetti. Ciò sta suggerendo un livello più profondo, anche al di là di Tell-Dont-Ask, e voglio aggiungere una breve osservazione al riguardo.

Per quanto riguarda la progettazione architettonica, si potrebbe desiderare di avere oggetti che sono in realtà solo contenitori per proprietà, forse anche immutabili, e quindi eseguire varie funzioni su raccolte di tali oggetti, valutandoli, filtrandoli o trasformandoli invece di inviarli comandi (che è più il dominio di Tell-Dont-Ask).

La decisione più appropriata per il tuo problema dipende dal fatto che ti aspetti di avere dati stabili (gli oggetti dichiarativi) ma con la modifica / aggiunta sul lato funzione. O se si prevede di avere un insieme stabile e limitato di tali funzioni, ma si aspettano più flusso a livello di oggetti, ad es. aggiungendo nuovi tipi. Nella prima situazione preferiresti le funzioni libere, nei metodi del secondo oggetto.

Bob Martin, nel suo libro "Codice pulito", chiama questo "Anti-Simmetria dati / oggetto" (p.95ff), altre comunità potrebbero riferirsi ad esso come " problema di espressione ".

    
risposta data 22.07.2014 - 15:12
fonte
3

Questo paradigma viene talvolta definito come "Dillo, non chiedere" , che significa dire all'oggetto cosa fare, non chiedere del suo stato; e talvolta come 'Chiedi, non dire' , che significa chiedere all'oggetto di fare qualcosa per te, non dirgli quale dovrebbe essere il suo stato. In ogni caso, la migliore pratica è la stessa: il modo in cui un oggetto deve eseguire un'azione è la preoccupazione dell'oggetto, non quella dell'oggetto chiamante. Le interfacce dovrebbero evitare di esporre il loro stato (ad esempio tramite accessor o proprietà pubbliche) ed esporre invece metodi 'doing' la cui implementazione è opaca. Altri hanno coperto questo link con il programmatore pragmatico.

Questa regola è correlata alla regola relativa all'elusione del codice "doppio punto" o "doppia freccia", spesso denominato "Parla solo agli amici immediati", che afferma che foo->getBar()->doSomething() è errato, invece usa foo->doSomething(); che è un richiamo del wrapper attorno alla funzionalità della barra e implementato come semplicemente return bar->doSomething(); - se foo è responsabile della gestione di bar , allora lascia che lo faccia!

    
risposta data 24.02.2016 - 15:38
fonte
1

Oltre alle altre buone risposte su "racconta, non chiedere", alcuni commenti sui tuoi esempi specifici che potrebbero aiutarti:

The advice in C++ is that you should prefer free functions instead of member functions as they increase encapsulation. Both of these are identical semantically, so why prefer the choice that has access to more state?

Questa scelta non ha accesso a più stati. Entrambi usano la stessa quantità di stato per fare il loro lavoro, ma l'esempio "cattivo" richiede che lo stato della classe sia pubblico per poter fare il suo lavoro. Inoltre, il comportamento di quella classe nell'esempio "cattivo" si estende alla funzione libera, rendendo più difficile la ricerca e più difficile da refactoring.

Why is it the responsibility of User to format an unrelated error string? What if I want to do something besides print 'No street name on file' if it has no street? What if the street is named the same thing?

Perché è responsabilità di 'nome_strada' fare sia 'ottenere il nome della via' sia 'fornire un messaggio di errore'? Almeno nella versione "buona", ogni pezzo ha una sola responsabilità. Tuttavia, non è un ottimo esempio.

    
risposta data 20.07.2012 - 03:37
fonte
1

Queste risposte sono molto buone, ma ecco un altro esempio da sottolineare: si noti che di solito è un modo per evitare la duplicazione. Ad esempio, supponiamo di avere più posti con un codice come:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Ciò significa che è meglio avere qualcosa di simile:

Product product = productMgr.get(productUuid, currentUser)

Perché questa duplicazione significa che la maggior parte dei client dell'interfaccia useranno il nuovo metodo, invece di ripetere la stessa logica qua e là. Dai al tuo delegato il lavoro che vuoi svolgere, invece di chiedere le informazioni che ti servono per farlo da solo.

    
risposta data 13.01.2016 - 17:35
fonte
0

Credo che questo sia più vero quando si scrive oggetto di alto livello, ma meno vero quando si passa al livello più profondo, ad es. libreria di classi in quanto è impossibile scrivere ogni singolo metodo per soddisfare tutti i consumatori di classe.

Per esempio # 2, penso che sia troppo semplificato. Se in realtà dovessimo implementarlo, il SystemMonitor finirebbe per avere il codice per l'accesso hardware di basso livello e la logica per l'astrazione di alto livello incorporata nella stessa classe. Sfortunatamente, se stiamo cercando di separarlo in due classi, violeremmo "Tell, Do not ask" stesso.

L'esempio # 4 è più o meno lo stesso: incorpora la logica dell'interfaccia utente nel livello dati. Ora, se vogliamo correggere ciò che l'utente vuole vedere in caso di assenza di indirizzo, dobbiamo correggere l'oggetto nel livello dati e cosa succede se due progetti utilizzano questo stesso oggetto, ma devono usare testo diverso per l'indirizzo nullo?

Sono d'accordo che se possiamo implementare "Tell, Do not ask" per tutto, sarebbe molto utile - io stesso sarei felice se potessi dire piuttosto che chiedere (e farlo da solo) nella vita reale ! Tuttavia, come nella vita reale, la fattibilità della soluzione è molto limitata alle classi di alto livello.

    
risposta data 20.07.2012 - 19:38
fonte

Leggi altre domande sui tag