Come scegliere tra Tell not Ask e Command Query Separation?


25

Il principio Tell Don't Ask dice:

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.

Un semplice esempio di "Tell, Don't Ask" è

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

e la versione tell è ...

Widget w = ...;
w.removeFromParent();

E se avessi bisogno di sapere il risultato dal metodo removeFromParent? La mia prima reazione è stata solo quella di cambiare removeFromParent per restituire un valore booleano che indica se il genitore è stato rimosso o meno.

Ma poi mi sono imbattuto in un modello di separazione delle query di comando che dice di NON farlo.

Indica che ogni metodo dovrebbe essere un comando che esegue un'azione o una query che restituisce dati al chiamante, ma non entrambi. In altre parole, porre una domanda non dovrebbe cambiare la risposta. Più formalmente, i metodi dovrebbero restituire un valore solo se sono referenzialmente trasparenti e quindi non presentano effetti collaterali.

Questi due sono davvero in contrasto tra loro e come faccio a scegliere tra i due? Vado con il programmatore pragmatico o Bertrand Meyer su questo?


1
cosa faresti con il booleano?
David,

1
Sembra che ti stai immergendo troppo in schemi di codifica senza bilanciare l'usabilità
Izkata

Per quanto riguarda il booleano ... questo è un esempio facile da salvare ma simile all'operazione di scrittura in basso, l'obiettivo è che sia uno stato dell'operazione.
Dakotah North

2
il punto è che non dovresti concentrarti sul "ritorno a qualcosa" ma piuttosto su cosa vuoi farne. Come ho detto in un altro commento, se ti concentri sul fallimento, quindi usa l'eccezione, se vuoi fare qualcosa quando l'operazione è completa, quindi usa callback o eventi, se vuoi registrare ciò che è successo è responsabilità del Widget ... perché sto chiedendo le tue esigenze. Se non riesci a trovare un esempio concreto in cui devi restituire qualcosa, forse significa che non dovrai scegliere tra i due.
David

1
La query di comando La separazione è un principio, non un modello. I pattern sono qualcosa che puoi usare per risolvere un problema. I principi sono ciò che segui per non creare problemi.
candied_orange,

Risposte:


25

In realtà il tuo problema di esempio illustra già la mancanza di decomposizione.

Cambiamolo un po ':

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Questo non è molto diverso, ma rende più evidente il difetto: perché il libro dovrebbe sapere del suo scaffale? In poche parole, non dovrebbe. Introduce una dipendenza di libri sugli scaffali (che non ha senso) e crea riferimenti circolari. Va tutto male.

Allo stesso modo non è necessario che un widget conosca il suo genitore. Dirai: "Ok, ma il widget ha bisogno del suo genitore per impaginarsi correttamente ecc." e sotto il cofano il widget conosce il suo genitore e gli chiede le sue metriche per calcolare le proprie metriche basate su di esse, ecc. Secondo quanto detto, non chiedere che sia sbagliato. Il genitore dovrebbe dire a tutti i suoi figli di renderizzare, passando tutte le informazioni necessarie come argomenti. In questo modo si può facilmente avere lo stesso widget in due genitori (indipendentemente dal fatto che ciò abbia effettivamente senso).

Per tornare all'esempio del libro, inserisci il bibliotecario:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

Per favore, capisci che non stiamo più chiedendo nel senso di dire, non chiedere .

Nella prima versione, lo scaffale era una proprietà del libro e tu l'hai chiesto. Nella seconda versione, non facciamo simili ipotesi sul libro. Tutto ciò che sappiamo è che il bibliotecario può dirci uno scaffale che contiene il libro. Presumibilmente si affida a una sorta di tabella di ricerca per farlo, ma non lo sappiamo per certo (potrebbe anche scorrere tutti i libri in ogni scaffale) e in realtà non vogliamo saperlo .
Non facciamo affidamento sul fatto se o come la risposta dei bibliotecari sia accoppiata al suo stato o a quello di altri oggetti da cui potrebbe dipendere. Diciamo al bibliotecario di trovare lo scaffale.
Nel primo scenario, abbiamo codificato direttamente la relazione tra un libro e uno scaffale come parte dello stato del libro e recuperato direttamente questo stato. Questo è molto fragile, perché ipotizziamo anche che lo scaffale restituito dal libro contenga il libro (questo è un vincolo che dobbiamo garantire, altrimenti potremmo non essere in grado di leggere removeil libro dallo scaffale in cui si trova effettivamente).

Con l'introduzione del bibliotecario, modelliamo questa relazione separatamente, ottenendo quindi la separazione delle preoccupazioni.


Penso che questo sia un punto molto valido, ma non risponde affatto alla domanda. Nella tua versione la domanda è: il metodo remove (Book b) nella classe Shelf dovrebbe avere un valore di ritorno?
scarfridge,

1
@scarfridge: alla fine, dì, non chiedere è un corollario della corretta separazione delle preoccupazioni. Se ci pensate, rispondo quindi alla domanda. Almeno la penso così;)
back2dos

1
@scarfridge Invece di un valore di ritorno, si potrebbe passare una funzione / delegato che viene chiamato in caso di errore. Se ha successo, hai finito, giusto?
Benedici Yahu il

2
@BlessYahu Mi sembra troppo ingegnerizzato. Personalmente ritengo che la separazione Command-query sia più di un ideale nel mondo reale (multi-thread). IMHO, va bene che un metodo con effetti collaterali restituisca un valore purché il nome del metodo indichi chiaramente che cambierà lo stato dell'oggetto. Considera il metodo pop () di uno stack. Ma un metodo con un nome di query non dovrebbe avere effetti collaterali.
scarfridge,

13

Se hai bisogno di conoscere il risultato, allora lo fai; questo è il tuo requisito.

La frase "i metodi dovrebbero restituire un valore solo se sono referenzialmente trasparenti e quindi non hanno effetti collaterali" è una buona linea guida da seguire (soprattutto se stai scrivendo il tuo programma in uno stile funzionale , per concorrenza o altri motivi), ma è non un assoluto.

Ad esempio, potrebbe essere necessario conoscere il risultato di un'operazione di scrittura del file (vero o falso). Questo è un esempio di un metodo che restituisce un valore, ma produce sempre effetti collaterali; non c'è modo di aggirare questo.

Per soddisfare la separazione di comandi / query, è necessario eseguire l'operazione sul file con un metodo, quindi verificarne il risultato con un altro metodo, una tecnica indesiderabile perché disaccoppia il risultato dal metodo che ha causato il risultato. Cosa succede se succede qualcosa allo stato dell'oggetto tra la chiamata al file e il controllo dello stato?

La linea di fondo è questa: se stai usando il risultato di una chiamata di metodo per prendere decisioni altrove nel programma, allora non stai violando Tell Don't Ask. Se, d'altra parte, stai prendendo decisioni per un oggetto basato su una chiamata del metodo a quell'oggetto, allora dovresti spostare quelle decisioni nell'oggetto stesso per preservare l'incapsulamento.

Ciò non è affatto contraddittorio con la separazione delle query di comando; in effetti, lo impone ulteriormente, perché non è più necessario esporre un metodo esterno a fini di stato.


1
Si potrebbe sostenere che nel tuo esempio, se l'operazione "write" fallisce, dovrebbe essere generata un'eccezione, quindi non è necessario un metodo "get status".
David,

@David: Quindi ripiegare sull'esempio del PO, che ritornerebbe vero se si verificasse effettivamente una modifica, falso se non lo fosse. Dubito che vorresti lanciare un'eccezione lì.
Robert Harvey,

Anche l'esempio del PO non mi sembra giusto. O vuoi essere notato se la rimozione non è stata possibile, nel qual caso dovresti usare l'eccezione o vuoi essere notato quando un widget viene effettivamente rimosso, nel qual caso potresti usare il meccanismo di eventi o callback. Semplicemente non vedo un vero esempio in cui non vorresti rispettare "Schema di separazione delle query di comando"
David

@ David: ho modificato la mia risposta per chiarire.
Robert Harvey,

Per non dire un punto troppo fine, ma l'idea alla base della separazione comando-query è che chiunque emetta il comando per scrivere il file non si preoccupa del risultato, almeno non in un senso specifico (un utente potrebbe in seguito visualizza un elenco di tutte le operazioni recenti sui file, ma che non ha alcuna correlazione con il comando iniziale). Se è necessario intraprendere azioni dopo il completamento di un comando, indipendentemente dal fatto che ci si preoccupi o meno del risultato, il modo corretto di farlo è con un modello di programmazione asincrono utilizzando eventi che (potrebbero) contenere dettagli sul comando originale e / o risultato.
Aaronaught l'

2

Penso che dovresti seguire il tuo istinto iniziale. A volte il design dovrebbe essere intenzionalmente pericoloso, per introdurre immediatamente complessità quando si comunica con l'oggetto in modo che il tuo forzato lo maneggi correttamente subito, invece di provare a gestirlo sull'oggetto stesso e nascondere tutti i dettagli cruenti e renderlo difficile da pulire cambiare le ipotesi sottostanti.

Dovresti avere una sensazione di affondamento se un oggetto ti offre equivalenti opene closecomportamenti implementati e inizi immediatamente a capire con cosa hai a che fare quando vedi un valore di ritorno booleano per qualcosa che ritieni possa essere un semplice compito atomico.

Come ti occuperai in seguito è la tua passione. Puoi creare un'astrazione sopra di esso, un tipo di widget con removeFromParent(), ma dovresti sempre avere un fallback di basso livello, altrimenti potresti aver fatto ipotesi premature.

Non provare a rendere tutto semplice. Non c'è nulla di così deludente quando ti affidi a qualcosa che sembra elegante e così innocente, solo per rendersi conto che è vero orrore in seguito nei momenti peggiori.


2

Ciò che descrivi è una "eccezione" nota al principio di separazione comando-query.

In questo articolo Martin Fowler spiega come minaccerebbe tale eccezione.

A Meyer piace usare assolutamente la separazione comando-query, ma ci sono eccezioni. Il blocco di uno stack è un buon esempio di query che modifica lo stato. Meyer afferma correttamente che puoi evitare di avere questo metodo, ma è un linguaggio utile. Quindi preferisco seguire questo principio quando posso, ma sono pronto a romperlo per ottenere il mio pop.

Nel tuo esempio considererei la stessa eccezione.


0

La separazione comandi-query è incredibilmente facile da fraintendere.

Se ti dico nel mio sistema c'è un comando che restituisce anche un valore da una query e dici "Ah! Sei in violazione!" stai saltando la pistola.

No, non è ciò che è proibito.

Ciò che è proibito è quando quel comando è l'UNICO modo per fare quella query. No. Non dovrei cambiare stato per porre domande. Ciò non significa che devo chiudere gli occhi ogni volta che cambio stato.

Non importa se lo faccio poiché li riaprirò comunque. L'unico vantaggio di questa reazione eccessiva è stato garantire che i progettisti non diventassero pigri e dimenticassero di includere la query che modifica lo stato.

Il vero significato è così dannatamente sottile che per i pigri è più facile dire che i setter dovrebbero tornare vuoti. Questo rende più facile essere sicuri di non violare la vera regola, ma è una reazione eccessiva che può diventare un vero dolore.

Le interfacce fluide e iDSL "violano" continuamente questa reazione eccessiva. C'è molto potere che viene ignorato se si reagisce in modo eccessivo.

Si può sostenere che un comando dovrebbe fare solo una cosa e una query dovrebbe fare solo una cosa. E in molti casi questo è un buon punto. Chi può dire che esiste una sola query che dovrebbe seguire un comando? Bene, ma non si tratta di separazione query-comando. Questo è il principio della singola responsabilità.

Guardato in questo modo e uno stack pop non è un'eccezione bizzarra. È una singola responsabilità. Pop viola la separazione delle query di comando solo se si dimentica di fornire una sbirciatina.

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.