¿Cómo elegir entre Separar no preguntar y Separar consultas de comando?

25

El principio Tell Don't Ask dice:

debes esforzarte por decirle a los objetos lo que quieres que hagan; no les haga preguntas sobre su estado, tome una decisión y luego dígales qué hacer.

El problema es que, como la persona que llama, no debe tomar decisiones basadas en el estado del objeto llamado que provoque que cambie el estado del objeto. La lógica que está implementando es probablemente la responsabilidad del objeto llamado, no la suya. Para que usted tome decisiones fuera del objeto viola su encapsulación.

Un ejemplo simple de "Dile, no preguntes" es

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

y la versión tell es ...

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

Pero, ¿qué sucede si necesito saber el resultado del método removeFromParent? Mi primera reacción fue simplemente cambiar removeFromParent para devolver un valor booleano que indica si el padre fue eliminado o no.

Pero luego me encontré con el Patrón de separación de consulta de comando que dice NO hacer esto.

Establece que cada método debe ser un comando que realiza una acción o una consulta que devuelve datos al llamante, pero no ambos. En otras palabras, hacer una pregunta no debería cambiar la respuesta. Más formalmente, los métodos deberían devolver un valor solo si son referencialmente transparentes y, por lo tanto, no poseen efectos secundarios.

¿Están estos dos realmente en desacuerdo entre sí y cómo elijo entre los dos? ¿Voy con el programador pragmático o Bertrand Meyer en esto?

Dakotah North
fuente
1
¿Qué harías con el booleano?
David
1
Parece que te estás sumergiendo demasiado en los patrones de codificación sin equilibrar la usabilidad
Izkata
Re booleano ... este es un ejemplo que fue fácil de recuperar pero similar a la operación de escritura a continuación, el objetivo es que sea un estado de la operación.
Dakotah Norte el
2
mi punto es que no debes concentrarte en "devolver algo" sino más bien en lo que quieres hacer con él. Como dije en otro comentario, si te enfocas en la falla, usa una excepción, si quieres hacer algo cuando se completa la operación, luego usa la devolución de llamada o los eventos, si quieres registrar lo que sucedió es responsabilidad del widget ... Eso es Por eso estoy preguntando por tus necesidades. Si no puede encontrar un ejemplo concreto en el que necesite devolver algo, tal vez eso significa que no tendrá que elegir entre los dos.
David
1
La separación de consultas de comandos es un principio, no un patrón. Los patrones son algo que puedes usar para resolver un problema. Los principios son lo que sigue para no crear problemas.
candied_orange

Respuestas:

25

En realidad, su problema de ejemplo ya ilustra la falta de descomposición.

Vamos a cambiarlo un poco:

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

Esto no es realmente diferente, pero hace que la falla sea más evidente: ¿por qué el libro sabría sobre su estante? En pocas palabras, no debería. Introduce una dependencia de libros en los estantes (lo que no tiene sentido) y crea referencias circulares. Todo esta mal.

Del mismo modo, no es necesario que un widget conozca a su padre. Dirá: "Ok, pero el widget necesita que su padre se distribuya correctamente, etc." y debajo del capó, el widget conoce a su padre y le pide sus métricas para calcular sus propias métricas basadas en ellas, etc. Según lo dicho, no pregunte que esto está mal. El padre debe decirle a todos sus hijos que rindan, pasando toda la información necesaria como argumentos. De esta manera, uno puede tener fácilmente el mismo widget en dos padres (ya sea que tenga sentido o no).

Para volver al ejemplo del libro, ingrese el bibliotecario:

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

Por favor, comprenda que ya no estamos preguntando en el sentido de decir, no pregunte .

En la primera versión, el estante era una propiedad del libro y usted lo solicitó. En la segunda versión, no hacemos tal suposición sobre el libro. Todo lo que sabemos es que el bibliotecario puede decirnos un estante que contiene el libro. Presumiblemente, se basa en algún tipo de tabla de búsqueda para hacerlo, pero no lo sabemos con certeza (también podría simplemente recorrer todos los libros en cada estante) y, en realidad , no queremos saberlo .
No confiamos en si la respuesta de los bibliotecarios está acoplada a su estado o el de otros objetos de los que dependa. Le decimos al bibliotecario que busque el estante.
En el primer escenario, codificamos directamente la relación entre un libro y un estante como parte del estado del libro y recuperamos este estado directamente. Esto es muy frágil, porque también suponemos que el estante devuelto por el libro contiene el libro (esta es una restricción que debemos garantizar, de lo contrario podríamos no poder hacer removeel libro desde el estante en el que realmente está).

Con la introducción del bibliotecario, modelamos esta relación por separado, logrando así la separación de la preocupación.

back2dos
fuente
Creo que este es un punto muy válido, pero no responde a la pregunta en absoluto. En su versión, la pregunta es, ¿debería el método remove (Libro b) en la clase Shelf tener un valor de retorno?
scarfridge
1
@scarfridge: en el fondo, decir, no preguntar es un corolario de la separación adecuada de las preocupaciones. Si lo piensas, entonces respondo la pregunta. Al menos eso creo;)
back2dos
1
@scarfridge En lugar de un valor de retorno, uno podría pasar una función / delegado que se llama en caso de falla. Si tiene éxito, ya está, ¿verdad?
Bendice a Yahu el
2
@BlessYahu Eso me parece excesivamente diseñado. Personalmente, creo que la separación de consulta de comandos es más ideal en el mundo real (multiproceso). En mi humilde opinión, está bien que un método con efectos secundarios devuelva un valor siempre que el nombre del método indique claramente que cambiará el estado del objeto. Considere el método pop () de una pila. Pero un método con un nombre de consulta no debería tener efectos secundarios.
scarfridge
13

Si necesita saber el resultado, entonces lo sabe; Ese es tu requisito.

La frase "los métodos deberían devolver un valor solo si son referencialmente transparentes y, por lo tanto, no poseen efectos secundarios" es una buena guía a seguir (especialmente si está escribiendo su programa en un estilo funcional , por concurrencia u otras razones), pero es No es un absoluto.

Por ejemplo, es posible que necesite saber el resultado de una operación de escritura de archivo (verdadero o falso). Ese es un ejemplo de un método que devuelve un valor, pero siempre produce efectos secundarios; no hay forma de evitar eso.

Para completar la Separación de Comando / Consulta, tendría que realizar la operación de archivo con un método, y luego verificar su resultado con otro método, una técnica indeseable porque desacopla el resultado del método que causó el resultado. ¿Qué pasa si algo le sucede al estado de su objeto entre la llamada al archivo y la verificación de estado?

La conclusión es esta: si está utilizando el resultado de una llamada a un método para tomar decisiones en otra parte del programa, entonces no está violando Tell Don't Ask. Si, por otro lado, está tomando decisiones para un objeto en función de una llamada de método a ese objeto, entonces debe mover esas decisiones al objeto mismo para preservar la encapsulación.

Esto no es en absoluto contradictorio con la separación de consultas de comandos; de hecho, lo impone aún más, porque ya no es necesario exponer un método externo para propósitos de estado.

Robert Harvey
fuente
1
Se podría argumentar que en su ejemplo, si la operación de "escritura" falla, se debe lanzar una excepción, por lo que no hay necesidad de un método de "obtención de estado".
David
@David: luego recurra al ejemplo del OP, que devolvería verdadero si realmente ocurriera un cambio, falso si no fuera así. Dudo que quieras lanzar una excepción allí.
Robert Harvey
Incluso el ejemplo del OP no me parece adecuado. O desea que se le notifique si la eliminación no fue posible, en cuyo caso usaría una excepción o si se le notará cuando se elimine un widget, en cuyo caso podría usar un mecanismo de evento o devolución de llamada. Simplemente no veo un ejemplo real en el que no quieras respetar el "Patrón de separación de consulta de comando"
David
@David: he editado mi respuesta para aclarar.
Robert Harvey
No para poner un punto demasiado fino en él, pero la idea detrás de la separación de consulta de comando es que a quien emita el comando para escribir el archivo no le importa el resultado, al menos, no en un sentido específico (un usuario podría más tarde obtenga una lista de todas las operaciones recientes de archivos, pero eso no tiene correlación con el comando inicial). Si necesita realizar acciones después de que se complete un comando, independientemente de si le importa el resultado o no, la forma correcta de hacerlo es con un modelo de programación asíncrono que utiliza eventos que (podrían) contener detalles sobre el comando original y / o su resultado.
Aaronaught
2

Creo que debes seguir tu instinto inicial. A veces, el diseño debe ser intencionalmente peligroso, para introducir complejidad de inmediato cuando te comunicas con el objeto de manera que puedas forzarlo a manejarlo de inmediato, en lugar de tratar de manejarlo en el objeto en sí mismo y ocultar todos los detalles sangrientos y dificultar la limpieza cambiar los supuestos subyacentes

Debería tener una sensación de hundimiento si un objeto le ofrece equivalentes implementados openy closecomportamientos e inmediatamente comienza a comprender a qué se enfrenta cuando ve un valor de retorno booleano para algo que pensó que sería una tarea atómica simple.

Cómo lidiar más adelante es lo tuyo. Puede crear una abstracción sobre ella, un tipo de widget con removeFromParent(), pero siempre debe tener un respaldo de bajo nivel, de lo contrario posiblemente estaría haciendo suposiciones prematuras.

No intentes simplificar todo. No hay nada tan decepcionante cuando confías en algo que parece elegante y tan inocente, solo para darte cuenta de que es un verdadero horror más tarde en los peores momentos.

Filip Dupanović
fuente
2

Lo que usted describe es una "excepción" conocida al principio de Separación de consulta de comando.

En este artículo, Martin Fowler explica cómo amenazaría con tal excepción.

A Meyer le gusta usar la separación de consulta de comando absolutamente, pero hay excepciones. Hacer estallar una pila es un buen ejemplo de una consulta que modifica el estado. Meyer dice correctamente que puedes evitar tener este método, pero es una expresión útil. Así que prefiero seguir este principio cuando puedo, pero estoy preparado para romperlo para obtener mi pop.

En su ejemplo, consideraría la misma excepción.

elviejo79
fuente
0

La separación de Comando-Consulta es increíblemente fácil de entender mal.

Si te digo en mi sistema, hay un comando que también devuelve un valor de una consulta y dices "¡Ja, estás en violación!" Estás saltando el arma.

No, eso no es lo que está prohibido.

Lo que está prohibido es cuando ese comando es la ÚNICA forma de hacer esa consulta. No. No debería tener que cambiar de estado para hacer preguntas. Eso no significa que tenga que cerrar los ojos cada vez que cambio de estado.

No importa si lo hago, ya que voy a abrirlos de nuevo de todos modos. El único beneficio de esta reacción exagerada fue garantizar que los diseñadores no se volvieran perezosos y olvidaran incluir la consulta que no cambia el estado.

El verdadero significado es tan sutil que es más fácil para la gente perezosa decir que los acomodadores deberían volver sin efecto. Esto hace que sea más fácil asegurarse de que no está violando la regla real, pero es una reacción exagerada que puede convertirse en un verdadero dolor.

Las interfaces fluidas y los iDSL "violan" esta reacción exagerada todo el tiempo. Se ignora mucho poder si reaccionas en exceso.

Puede argumentar que un comando debe hacer solo una cosa y una consulta debe hacer solo una cosa. Y en muchos casos ese es un buen punto. ¿Quién puede decir que solo hay una consulta que debe seguir un comando? Bien, pero eso no es separación de consulta de comando. Ese es el principio de responsabilidad única.

Visto de esta manera y un stack pop no es una extraña excepción. Es una sola responsabilidad. Pop solo viola la separación de consultas de comandos si olvida proporcionar un vistazo.

naranja confitada
fuente