Come scegliere tra Tell do not Ask e Command Query Separation?

25

Il principio Tell Do not Ask dice:

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.

Un semplice esempio di "Tell, don" "Chiedi" è

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

e la versione di tell è ...

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

Ma cosa succede se ho bisogno di sapere il risultato del metodo removeFromParent? La mia prima reazione è stata quella di cambiare removeFromParent per restituire un valore booleano che indica se il genitore è stato rimosso o meno.

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

It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, asking a question should not change the answer. More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.

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

    
posta Dakotah North 08.11.2011 - 16:39
fonte

5 risposte

24

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

Modifichiamolo un po ':

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

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

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

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

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

Ti preghiamo di capire che non stiamo chiedendo più nel senso di dire, non chiedere .

Nella prima versione, lo scaffale era una proprietà del libro e tu lo chiedi. Nella seconda versione, non facciamo una tale ipotesi sul libro. Tutto quello che sappiamo è che il bibliotecario può dirci una mensola che contiene il libro. Presumibilmente si affida a una sorta di tavolo di ricerca per farlo, ma non lo sappiamo con certezza (potrebbe anche scorrere tutti i libri in ogni scaffale) e in realtà non vogliamo sapere .
Non ci basiamo su come o come la risposta dei bibliotecari sia accoppiata al suo stato o a quella 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é anche noi supponiamo che lo scaffale restituito dal libro contenga il libro (questo è un vincolo che dobbiamo garantire, altrimenti potremmo non essere in grado di remove del libro dallo scaffale in cui si trova effettivamente) .

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

    
risposta data 08.11.2011 - 18:15
fonte
13

Se hai bisogno di sapere 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 guida da seguire (specialmente 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 (true o false). Questo è un esempio di un metodo che restituisce un valore, ma produce sempre effetti collaterali; non c'è modo di aggirare questo.

Per soddisfare la Comando / Query Separation, dovresti eseguire l'operazione sui file con un metodo, e quindi controllarne il risultato con un altro metodo, una tecnica indesiderabile perché disaccoppia il risultato dal metodo che ha causato il risultato. Che succede se succede qualcosa allo stato del tuo oggetto tra la chiamata al file e il controllo dello stato?

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

Questo non è affatto contraddittorio con Command Query Separation; in effetti, lo rafforza ulteriormente, perché non è più necessario esporre un metodo esterno ai fini dello stato.

    
risposta data 08.11.2011 - 16:52
fonte
2

Penso che dovresti seguire il tuo istinto iniziale. A volte il design dovrebbe essere intenzionalmente pericoloso, per introdurre immediatamente la complessità quando comunichi con l'oggetto in modo che il tuo manipolo sia gestito correttamente subito, invece di cercare di gestirlo sull'oggetto stesso e nascondere tutti i dettagli cruenti e renderlo difficile da pulire cambia le ipotesi sottostanti.

Dovresti ottenere una sensazione di affondamento se un oggetto ti offre implementati equivalenti di open e close comportamenti e inizi immediatamente a capire di cosa ti occupi quando vedi un valore di ritorno booleano per qualcosa che pensavi sarebbe stato un semplice compito atomico.

In che modo ti occupi più tardi è la tua cosa. Puoi creare un'astrazione sopra di essa, un tipo di widget con removeFromParent() , ma dovresti sempre avere un fallback di basso livello, altrimenti potresti aver fatto ipotesi premature.

Non cercare di rendere tutto semplice. Non c'è niente di più deludente quando ti affidi a qualcosa che sembra elegante e innocente, solo per rendersi conto che è il vero orrore più tardi nel peggiore dei momenti.

    
risposta data 08.11.2011 - 18:17
fonte
2

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

In questo articolo Martin Fowler spiega come potrebbe minacciare tale eccezione.

Meyer likes to use command-query separation absolutely, but there are exceptions. Popping a stack is a good example of a query that modifies state. Meyer correctly says that you can avoid having this method, but it is a useful idiom. So I prefer to follow this principle when I can, but I'm prepared to break it to get my pop.

Nel tuo esempio considererei la stessa eccezione.

    
risposta data 27.08.2016 - 21:12
fonte
0

La separazione della query di comando è incredibilmente facile da fraintendere.

Se ti dico che nel mio sistema c'è un comando che restituisce anche un valore da una query e tu dici "Ha! stai violando!" 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 fare domande. Ciò non significa che devo chiudere gli occhi ogni volta che cambio stato.

Non importa se lo faccio dato che sto per riaprirli comunque. L'unico vantaggio di questa reazione eccessiva è stato quello di garantire che i progettisti non diventassero pigri e dimenticassero di includere la query di modifica non statica.

Il vero significato è talmente dannatamente sottile che è più facile per i pigri dire che i setter dovrebbero restituire il nulla. Ciò rende più facile essere certi di non violare la regola reale, ma è una reazione eccessiva che può diventare un vero dolore.

Le interfacce fluenti e iDSL "violano" questa reazione eccessiva in ogni momento. C'è un sacco di potere che viene ignorato se si reagisce troppo.

Si potrebbe obiettare 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 c'è sempre una sola query che dovrebbe seguire un comando? Bene, ma non si tratta di una separazione tra query e comandi. Questo è il principio della responsabilità unica.

Visto in questo modo e uno stack pop non è una bizzarra eccezione. È una singola responsabilità. Pop interrompe solo la separazione delle query dei comandi se ti dimentichi di dare un'occhiata.

    
risposta data 27.08.2016 - 22:05
fonte

Leggi altre domande sui tag