Spiegazione su come "Tell, Don't Ask" è considerato buono OO


49

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

Come esempio n. 2:

Male:

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 gratuite invece delle funzioni membro poiché aumentano l'incapsulamento. Entrambi sono identici semanticamente, quindi perché preferire la scelta che ha accesso a più stati?

Esempio 4:

Male:

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à Userformattare una stringa di errore non correlata? E se volessi fare qualcosa oltre a stampare 'No street name on file'se non ha strada? E se la strada si chiamasse la stessa cosa?


Qualcuno potrebbe illuminarmi sui vantaggi e la logica di "Tell, Don't Ask"? Non sto cercando quale sia meglio, ma invece cerco di capire il punto di vista dell'autore.


Esempi di codice potrebbero essere Ruby e non Python, non lo so.
Pubblicazione del

2
Mi chiedo sempre se qualcosa come il primo esempio non è piuttosto una violazione di SRP?
stijn


Rubino. @ è una scorciatoia per esempio e Python termina implicitamente i suoi blocchi con spazi bianchi.
Erik Reppen,

3
"Il consiglio in C ++ è che dovresti preferire le funzioni libere invece delle funzioni membro poiché aumentano l'incapsulamento." Non so chi te lo abbia detto, ma non è vero. Le funzioni libere possono essere utilizzate per aumentare l'incapsulamento, ma non aumentano necessariamente l'incapsulamento.
Rob K,

Risposte:


81

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; parte del suo comportamento si trova al di fuori dell'oggetto e lo stato interno è esposto (forse inutilmente) al mondo esterno.

Dovresti cercare di dire agli oggetti cosa vuoi che facciano; non porre loro domande sul loro stato, prendere una decisione e poi dire loro cosa fare.

Il problema è che, come chiamante, non dovresti prendere decisioni basate sullo stato dell'oggetto chiamato che ti porta a cambiare lo stato dell'oggetto. La logica che stai implementando è probabilmente la responsabilità dell'oggetto chiamato, non la tua. Per te prendere decisioni al di fuori dell'oggetto viola la sua incapsulamento.

Certo, potresti dire, è ovvio. Non scriverei mai un codice del genere. Tuttavia, è molto facile lasciarsi cullare dall'esame di alcuni oggetti di riferimento e quindi chiamare diversi metodi in base ai risultati. Ma questo potrebbe non essere il modo migliore per farlo. Di 'all'oggetto quello che vuoi. Lascia che capisca come farlo. Pensa in modo dichiarativo anziché procedurale!

È più facile rimanere fuori da questa trappola se si inizia progettando classi in base alle loro responsabilità; puoi quindi progredire naturalmente nello specificare i comandi che la classe può eseguire, al contrario delle query che ti informano sullo stato dell'oggetto.

http://pragprog.com/articles/tell-dont-ask


4
Il testo di esempio non consente molte cose che sono chiaramente buone pratiche.
DeadMG

13
@DeadMG fa quello che dici solo a quelli che lo seguono con slavazione, che ignorano ciecamente "pragmatico" nel nome del sito e il pensiero chiave degli autori del sito che è stato chiaramente affermato nel loro libro chiave: "non esiste una soluzione migliore ... "
moscerino il

2
Non leggere mai il libro. Né vorrei. Ho letto solo il testo di esempio, che è completamente corretto.
DeadMG

3
@DeadMG non preoccuparti. Ora che conosci il punto chiave che inserisce questo esempio (e qualsiasi altro da pragprog per quella materia) nel contesto previsto ("nessuna cosa come una soluzione migliore ..."), va bene non leggere il libro
gnat

1
Non sono ancora sicuro di ciò che Tell, Don't Ask dovrebbe spiegarti senza contesto, ma questo è davvero un buon consiglio OOP.
Erik Reppen,

16

In generale, il pezzo suggerisce che non dovresti esporre lo stato membro per gli altri a ragionare, se potessi ragionare tu stesso .

Tuttavia, ciò che non è chiaramente affermato è che questa legge cade in limiti molto evidenti quando il ragionamento supera la responsabilità di una classe specifica. Ad esempio, ogni classe il cui compito è mantenere un 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 la temperaturequery come, quindi domani, il client può check_for_underheatingsenza dover cambiare SystemMonitor. Questo non è il caso in cui l' SystemMonitorattrezzo check_for_overheatingstesso. Pertanto, una SystemMonitorclasse il cui compito è generare un allarme quando la temperatura è troppo alta lo segue, ma una SystemMonitorclasse il cui compito è consentire a un altro pezzo di codice di leggere la temperatura in modo che possa controllare, diciamo, TurboBoost o qualcosa del genere , non dovrebbe.

Si noti inoltre che il secondo esempio utilizza inutilmente l'anti-pattern Null Object.


19
"Null object" non è quello che definirei un anti-pattern, quindi mi chiedo qual è la tua ragione per farlo?
Konrad Rudolph,

4
Abbastanza sicuro che nessuno abbia alcun metodo specificato come "Non fa nulla". Questo rende le chiamate un po 'inutili. Ciò significa che qualsiasi oggetto che implementa Null Object rompe almeno LSP e si descrive come operazioni di implementazione che, in realtà, non lo fa. L'utente si aspetta un valore indietro. La correttezza del loro programma dipende da questo. Semplicemente porti più problemi fingendo che sia un valore quando non lo è. Hai mai provato a eseguire il debug dei metodi silenziosamente non riusciti? È impossibile e nessuno dovrebbe mai soffrire per quello.
DeadMG

4
Direi che questo dipende interamente dal dominio del problema.
Konrad Rudolph,

5
@DeadMG Sono d'accordo che l'esempio di cui sopra è un cattivo utilizzo del modello Null Object, ma v'è un merito di usarlo. Alcune volte ho usato un'implementazione "no-op" di alcune interfacce o altre per evitare il controllo nullo o avere un vero "null" che permeava il sistema.
Max

6
Non sono sicuro di vedere il tuo punto con "il cliente può check_for_underheatingsenza dover cambiare SystemMonitor". In che modo il cliente è diverso da SystemMonitorquel punto? Non stai dissipando la tua logica di monitoraggio su più classi? Inoltre non vedo il problema con una classe di monitor che fornisce informazioni sui sensori ad altre classi riservando le funzioni di allarme a se stesso. Il controller boost dovrebbe controllare boost senza doversi preoccupare di generare un allarme se la temperatura aumenta troppo.
TMN,

9

Il vero problema con il tuo esempio di surriscaldamento è che le regole per ciò che si qualifica come surriscaldamento non sono facilmente variabili per sistemi diversi. Supponiamo che il Sistema A sia come ce l'hai (temp> 100 sta surriscaldando) ma System B è più delicato (temp> 93 sta surriscaldando). Modifichi la tua funzione di controllo per verificare il tipo di sistema e quindi applicare 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 definire la sua capacità di riscaldamento?

MODIFICARE:

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 rende brutta la tua funzione di controllo quando inizi a gestire più sistemi. Quest'ultimo consente alla funzione di controllo di essere stabile col passare del tempo.


1
Perché non registrare ciascun sistema con il monitor? Durante la registrazione possono indicare quando si verifica il surriscaldamento.
Martin York,

@LokiAstari - Potresti, ma poi potresti imbatterti in un nuovo sistema sensibile anche all'umidità o alla pressione atmosferica. Il principio è quello di sottrarre ciò che varia - in questo caso è la suscettibilità al surriscaldamento
Matthew Flynn,

1
Questo è esattamente il motivo per cui dovresti avere un modello tell. Indichi al sistema le condizioni attuali e ti informa se è al di fuori delle normali condizioni di lavoro. In questo modo non è mai necessario modificare SystemMoniter. Questo è incapsulamento per te.
Martin York,

@LokiAstari - Penso che stiamo parlando a scopi incrociati qui - Stavo davvero cercando di creare sistemi diversi, piuttosto che monitor diversi. Il fatto è che il sistema dovrebbe sapere quando si trova in uno stato che genera un allarme, al contrario di alcune funzioni del controller esterno. SystemA dovrebbe avere i suoi criteri, SystemB dovrebbe avere i suoi. Il controller dovrebbe essere in grado di chiedere (a intervalli regolari) se il sistema è OK o meno.
Matthew Flynn,

6

Prima di tutto, sento di dover fare un'eccezione alla tua caratterizzazione degli esempi come "cattivo" e "buono". L'articolo usa i termini "Non così buono" e "Migliore", penso che quei termini siano stati scelti per un motivo: si tratta di linee guida e, a seconda delle circostanze, l'approccio "Non così buono" può essere appropriato, o addirittura l'unica soluzione.

Quando ti viene data una scelta, dovresti privilegiare l'inclusione di qualsiasi funzionalità che si basa esclusivamente sulla classe nella classe anziché all'esterno di essa - il motivo è dovuto all'incapsulamento e al fatto che rende più semplice evolvere la classe nel tempo. La classe fa anche un lavoro migliore pubblicizzando le sue capacità rispetto a un mucchio 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 faccia la maggior parte degli utenti della classe. A volte vuoi dirlo, perché il comportamento è contro intuitivo per la classe e non vuoi confondere la maggior parte degli utenti della classe.

Ad esempio, ti lamenti dell'indirizzo stradale 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 trattasse di Stato o Città, è possibile che si desideri un valore predefinito quando si assegna un record a un venditore o a un partecipante al sondaggio, in modo che tutti gli incogniti vadano a una persona specifica. D'altra parte, se stavi stampando buste, potresti preferire un'eccezione o una protezione che ti impedisce di sprecare carta per lettere che non possono essere consegnate.

Quindi ci possono essere casi in cui "Not so good" è la strada da percorrere, ma in generale "Better" è, beh, meglio.


3

Anti-simmetria di dati / oggetti

Come altri hanno sottolineato, Tell-Dont-Ask è specifico per i casi in cui si cambia lo stato dell'oggetto dopo averlo chiesto (vedere ad esempio il testo di Pragprog pubblicato altrove in questa pagina). Questo non è sempre il caso, ad es. L'oggetto "user" non viene modificato dopo che è stato richiesto il suo user.address. È quindi discutibile se questo è un caso appropriato da applicare Tell-Dont-Ask.

Tell-Dont-Ask si occupa della responsabilità, non tirando fuori la logica da una classe che dovrebbe essere legittimamente all'interno di essa. Ma non tutta la logica che si occupa degli oggetti è necessariamente la logica di quegli oggetti. Questo è un suggerimento a un livello più profondo, anche oltre Tell-Dont-Ask, e voglio aggiungere una breve osservazione a riguardo.

Per quanto riguarda la progettazione architettonica, potresti voler avere oggetti che sono in realtà solo contenitori per proprietà, forse anche immutabili, e quindi eseguire varie funzioni su raccolte di tali oggetti, valutandole, filtrandole o trasformandole invece di inviare loro comandi (che è più il dominio di Tell-Dont-Ask).

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

Bob Martin, nel suo libro "Clean Code", lo chiama "Dati / Oggetto Anti-Simmetria" (p. 95ff), altre comunità potrebbero riferirsi ad esso come " problema dell'espressione ".


3

Questo paradigma viene talvolta definito 'Dillo, non chiedere' , che significa dire all'oggetto cosa fare, non chiedere del suo stato; e talvolta come 'Chiedi, non dirlo' , 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 dovrebbe eseguire un'azione è la preoccupazione di quell'oggetto, non quella dell'oggetto chiamante. Le interfacce dovrebbero evitare di esporre il loro stato (ad esempio tramite accessori o proprietà pubbliche) ed esporre invece metodi di "fare" la cui implementazione è opaca. Altri lo hanno coperto con i collegamenti al programmatore pragmatico.

Questa regola è correlata alla regola sull'evitare il codice "doppio punto" o "doppia freccia", spesso indicato come "Parla solo con amici immediati", che afferma che foo->getBar()->doSomething()è negativo, invece usa foo->doSomething();quale è una chiamata wrapper attorno alla funzionalità della barra e implementato semplicemente return bar->doSomething();: se fooè responsabile della gestione bar, allora fallo!


1

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

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

Tale scelta non ha accesso a più stati. Entrambi usano la stessa quantità di stato per fare il proprio lavoro, ma l'esempio "cattivo" richiede che lo stato della classe sia pubblico per fare il proprio lavoro. Inoltre, il comportamento di quella classe nell'esempio "cattivo" è esteso alla funzione libera, rendendo più difficile da trovare e più difficile da refactoring.

Perché è responsabilità dell'utente formattare una stringa di errore non correlata? E se volessi fare qualcosa oltre a stampare "Nessun nome di via in archivio" se non ha via? E se la strada si chiamasse la stessa cosa?

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


2
Non è vero. Presumi che il controllo del surriscaldamento sia l'unica cosa sensata da fare con la temperatura. Che cosa succede se la classe è destinata a essere uno dei numerosi monitor di temperatura e un sistema deve agire diversamente a seconda di molti dei loro risultati, ad esempio? Quando questo comportamento può essere limitato al comportamento predefinito di una singola istanza, allora certo. Altrimenti, ovviamente non può essere applicato.
DeadMG

Certo, o se il termostato e gli allarmi esistessero in classi diverse (come probabilmente dovrebbero).
Telastyn,

1
@DeadMG: il consiglio generale è di rendere le cose private / protette fino a quando non è necessario accedervi. Mentre questo esempio particolare è meh, ciò non contesta la pratica standard.
Guvante,

Un esempio in un articolo sulla pratica di essere "meh" in qualche modo lo contesta. Se questa pratica è standard a causa dei suoi grandi benefici, allora perché la difficoltà a trovare un esempio adatto?
Stijn de Witt,

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 che tu abbia 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 del genere:

Product product = productMgr.get(productUuid, currentUser)

Perché quella duplicazione significa che la maggior parte dei client della tua interfaccia userebbe il nuovo metodo, invece di ripetere la stessa logica qua e là. Dai al tuo delegato il lavoro che vuoi fare, invece di chiedere le informazioni di cui hai bisogno per farlo da solo.


0

Credo che questo sia più vero quando si scrive un oggetto di alto livello, ma meno vero quando si scende al livello più profondo, ad esempio la libreria di classi, poiché è impossibile scrivere ogni singolo metodo per soddisfare tutti i consumatori di classe.

Ad esempio, n. 2, penso che sia troppo semplificato. Se avessimo effettivamente implementato questo, SystemMonitor avrebbe avuto il codice per l'accesso hardware di basso livello e la logica per l'astrazione di alto livello integrata nella stessa classe. Sfortunatamente, se stiamo cercando di separarlo in due classi, violeremmo lo stesso "Tell, Don't ask".

L'esempio n. 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 nessun indirizzo, dobbiamo correggere l'oggetto nel livello dati e cosa succede se due progetti utilizzano lo stesso oggetto ma devono utilizzare un testo diverso per l'indirizzo null?

Sono d'accordo che se potessimo implementare "Tell, Don't ask" per tutto, sarebbe molto utile - io stesso sarei felice se potessi solo 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.

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.