Explicación sobre cómo "decir, no preguntar" se considera bueno OO

49

Esta publicación de blog se publicó en Hacker News con varios votos a favor. Viniendo de C ++, la mayoría de estos ejemplos parecen ir en contra de lo que me han enseñado.

Tal como el ejemplo # 2:

Malo:

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

versus bueno:

system_monitor.check_for_overheating

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

El consejo en C ++ es que debe preferir las funciones gratuitas en lugar de las funciones miembro, ya que aumentan la encapsulación. Ambos son idénticos semánticamente, entonces, ¿por qué preferir la opción que tiene acceso a más estado?

Ejemplo 4

Malo:

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

versus bueno:

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

¿Por qué es responsabilidad Userformatear una cadena de error no relacionada? ¿Qué pasa si quiero hacer algo además de imprimir 'No street name on file'si no tiene calle? ¿Qué pasa si la calle se llama igual?


¿Podría alguien aclararme sobre las ventajas y la lógica de "Tell, Don't Ask"? No busco cuál es mejor, sino que trato de entender el punto de vista del autor.

Pubby
fuente
Los ejemplos de código pueden ser Ruby y no Python, no lo sé.
Pubby
2
Siempre me pregunto si algo como el primer ejemplo no es más bien una violación de SRP.
stijn
1
Puede leer eso: pragprog.com/articles/tell-dont-ask
Mik378
Rubí. @ es una abreviatura, por ejemplo, y Python termina sus bloques implícitamente con espacios en blanco.
Erik Reppen
3
"El consejo en C ++ es que debería preferir las funciones gratuitas en lugar de las funciones miembro, ya que aumentan la encapsulación". No sé quién te dijo eso, pero no es cierto. Las funciones libres se pueden usar para aumentar la encapsulación, pero no necesariamente aumentan la encapsulación.
Rob K

Respuestas:

81

Preguntarle al objeto acerca de su estado y luego llamar a métodos sobre ese objeto basados ​​en decisiones tomadas fuera del objeto, significa que el objeto ahora es una abstracción permeable; parte de su comportamiento se encuentra fuera del objeto, y el estado interno está expuesto (quizás innecesariamente) al mundo exterior.

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.

Claro, puedes decir, eso es obvio. Nunca escribiría un código así. Aún así, es muy fácil dejarse llevar por el examen de algún objeto referenciado y luego llamar a diferentes métodos en función de los resultados. Pero esa puede no ser la mejor manera de hacerlo. Dile al objeto lo que quieres. Deja que descubra cómo hacerlo. ¡Piensa declarativamente en lugar de procedimentalmente!

Es más fácil mantenerse alejado de esta trampa si comienza diseñando clases basadas en sus responsabilidades; luego puede avanzar naturalmente para especificar comandos que la clase puede ejecutar, en lugar de consultas que le informan sobre el estado del objeto.

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

Robert Harvey
fuente
44
El texto de ejemplo no permite muchas cosas que son claramente una buena práctica.
DeadMG
13
@DeadMG hace lo que usted dice solo a aquellos que lo siguen servilmente, que ignoran ciegamente "pragmático" en el nombre del sitio y el pensamiento clave de los autores del sitio que se ha declarado claramente en su libro clave: "no existe la mejor solución ... "
mosquito
2
Nunca leas el libro. Tampoco desearía hacerlo. Solo leo el texto de ejemplo, que es completamente justo.
DeadMG
3
@DeadMG no te preocupes. Ahora que conoce el punto clave que pone este ejemplo (y cualquier otro de pragprog para el caso) en el contexto previsto ("no existe la mejor solución ..."), está bien no leer el libro
gnat
1
Todavía no estoy seguro de qué se supone que Tell, Don't Ask te explicará sin contexto, pero este es realmente un buen consejo de OOP.
Erik Reppen
16

En general, la pieza sugiere que no debe exponer el estado miembro para que otros razonen, si pudiera hacerlo usted mismo .

Sin embargo, lo que no está claramente establecido es que esta ley cae en límites muy obvios cuando el razonamiento supera la responsabilidad de una clase específica. Por ejemplo, cada clase cuyo trabajo es mantener algún valor o proporcionar algún valor, especialmente los genéricos, o donde la clase proporciona un comportamiento que debe extenderse.

Por ejemplo, si el sistema proporciona el temperaturecomo una consulta, mañana, el cliente puede check_for_underheatingsin tener que cambiar SystemMonitor. Este no es el caso cuando el SystemMonitorimplemento check_for_overheatingse implementa solo. Por lo tanto, una SystemMonitorclase cuyo trabajo es generar una alarma cuando la temperatura es demasiado alta sigue esto, pero una SystemMonitorclase cuyo trabajo es permitir que otro código lea la temperatura para que pueda controlar, por ejemplo, TurboBoost o algo así. , no debería.

También tenga en cuenta que el segundo ejemplo utiliza sin sentido el Anti-patrón de objetos nulos.

DeadMG
fuente
19
"Objeto nulo" no es lo que yo llamaría un antipatrón, así que me pregunto cuál es su razón para hacerlo.
Konrad Rudolph
44
Estoy bastante seguro de que nadie tiene ningún método que se especifique como "No hace nada". Eso hace que llamarlos sea un poco inútil. Eso significa que cualquier objeto que implemente un objeto nulo rompe LSP, como mínimo, y se describe a sí mismo como operaciones de implementación que, de hecho, no lo hace. El usuario espera un valor de vuelta. La corrección de su programa depende de ello. Simplemente trae más problemas pretendiendo que es un valor cuando no lo es. ¿Alguna vez has intentado depurar métodos silenciosamente fallidos? Es imposible y nadie debería sufrir por eso.
DeadMG
44
Yo diría que esto depende completamente del dominio del problema.
Konrad Rudolph
55
@DeadMG Estoy de acuerdo que el ejemplo anterior es un mal uso del patrón de objeto nulo, pero no es un mérito a usarlo. Algunas veces he usado una implementación 'no-op' de alguna interfaz u otra para evitar la verificación nula o tener un verdadero 'nulo' que impregne el sistema.
Max
66
No estoy seguro de ver su punto con "el cliente puede check_for_underheatingsin tener que cambiar SystemMonitor". ¿En qué se diferencia el cliente SystemMonitoren ese momento? ¿No estás disipando tu lógica de monitoreo en varias clases? Tampoco veo el problema con una clase de monitor que proporciona información del sensor a otras clases mientras reserva funciones de alarma para sí mismo. El controlador de impulso debería estar controlando el impulso sin tener que preocuparse por hacer sonar una alarma si la temperatura sube demasiado.
TMN
9

El problema real con su ejemplo de sobrecalentamiento es que las reglas para lo que califica como sobrecalentamiento no varían fácilmente para diferentes sistemas. Suponga que el Sistema A está como lo tiene (temp> 100 se está sobrecalentando) pero el Sistema B es más delicado (temp> 93 se está sobrecalentando). ¿Cambia su función de control para verificar el tipo de sistema y luego aplica el valor correcto?

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 tiene cada tipo de sistema que define su capacidad de calefacción?

EDITAR:

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

La primera forma hace que su función de control se vuelva fea a medida que comienza a trabajar con más sistemas. Este último permite que la función de control sea estable a medida que pasa el tiempo.

Matthew Flynn
fuente
1
¿Por qué no hacer que cada sistema se registre con el monitor? Durante el registro pueden indicar cuándo se produce el sobrecalentamiento.
Martin York
@LokiAstari: podría, pero luego podría encontrarse con un nuevo sistema que también sea sensible a la humedad o la presión atmosférica. El principio es resumir lo que varía, en este caso es la susceptibilidad al sobrecalentamiento
Matthew Flynn
1
Esto es exactamente por qué debería tener un modelo tell. Le informa al sistema las condiciones actuales y le informa si está fuera de las condiciones normales de trabajo. De este modo, nunca necesitará modificar el SystemMoniter. Eso es encapsulación para ti.
Martin York
@LokiAstari - Creo que estamos hablando de propósitos cruzados aquí - Realmente estaba buscando crear diferentes sistemas, en lugar de diferentes monitores. La cuestión es que el sistema debe saber cuándo se encuentra en un estado que genera una alarma, a diferencia de alguna función externa del controlador. SystemA debería tener sus criterios, SystemB debería tener los suyos. El controlador solo debe poder preguntar (a intervalos regulares) si el sistema está bien o no.
Matthew Flynn
6

En primer lugar, creo que debo hacer una excepción a su caracterización de los ejemplos como "malos" y "buenos". El artículo usa los términos "No tan bueno" y "Mejor", creo que esos términos fueron elegidos por una razón: estas son pautas, y dependiendo de las circunstancias, el enfoque "No tan bueno" puede ser apropiado, o incluso la única solución.

Cuando se le da una opción, debe dar preferencia a incluir cualquier funcionalidad que se base únicamente en la clase en la clase en lugar de fuera de ella; la razón se debe a la encapsulación y al hecho de que facilita la evolución de la clase con el tiempo. La clase también hace un mejor trabajo al anunciar sus capacidades que un montón de funciones gratuitas.

A veces tienes que decirlo, porque la decisión se basa en algo fuera de la clase o porque es simplemente algo que no quieres que hagan la mayoría de los usuarios de la clase. A veces quieres saberlo, porque el comportamiento es contra intuitivo para la clase, y no quieres confundir a la mayoría de los usuarios de la clase.

Por ejemplo, se queja de que la dirección de la calle devuelve un mensaje de error, no lo es, lo que está haciendo es proporcionar un valor predeterminado. Pero a veces un valor predeterminado no es apropiado. Si se tratara de un estado o una ciudad, es posible que desee un valor predeterminado al asignar un registro a un vendedor o encuestador, de modo que todas las incógnitas se dirijan a una persona específica. Por otro lado, si estaba imprimiendo sobres, es posible que prefiera una excepción o protección que evite que desperdicie papel en cartas que no se pueden entregar.

Por lo tanto, puede haber casos en los que "No tan bueno" es el camino a seguir, pero en general, "Mejor" es, bueno, mejor.

jmoreno
fuente
3

Antisimetría de datos / objetos

Como otros señalaron, Tell-Dont-Ask es específicamente para casos en los que cambia el estado del objeto después de haberlo preguntado (consulte, por ejemplo, el texto de Pragprog publicado en otra parte de esta página). Este no es siempre el caso, por ejemplo, el objeto 'usuario' no cambia después de que se le solicite su dirección de usuario. Por lo tanto, es discutible si este es un caso apropiado para aplicar Tell-Dont-Ask.

Tell-Dont-Ask tiene que ver con la responsabilidad, con no sacar la lógica de una clase que debería estar justificadamente dentro de ella. Pero no toda lógica que trata con objetos es necesariamente lógica de esos objetos. Esto está insinuando un nivel más profundo, incluso más allá de Tell-Dont-Ask, y quiero agregar un breve comentario al respecto.

Como cuestión de diseño arquitectónico, es posible que desee tener objetos que realmente sean solo contenedores para propiedades, incluso inmutables, y luego ejecutar varias funciones sobre colecciones de dichos objetos, evaluándolos, filtrándolos o transformándolos en lugar de enviarles comandos (que es más el dominio de Tell-Dont-Ask).

La decisión que sea más apropiada para su problema depende de si espera tener datos estables (los objetos declarativos) pero si cambia / agrega en el lado de la función. O si espera tener un conjunto estable y limitado de tales funciones pero espera más flujo a nivel de objetos, por ejemplo, agregando nuevos tipos. En la primera situación, preferiría funciones libres, en los métodos del segundo objeto.

Bob Martin, en su libro "Clean Code", llama a esto "Data / Object Anti-Symmetry" (p.95ff), otras comunidades podrían referirse a él como el " problema de expresión ".

ThomasH
fuente
3

Este paradigma a veces se denomina "decir, no preguntar" , lo que significa decirle al objeto qué hacer, no preguntar sobre su estado; y a veces como 'Preguntar, no decir' , lo que significa pedirle al objeto que haga algo por ti, no le digas cuál debería ser su estado. De cualquier manera, la mejor práctica es la misma: la forma en que un objeto debe realizar una acción es la preocupación de ese objeto, no la del objeto llamante. Las interfaces deben evitar exponer su estado (por ejemplo, a través de accesores o propiedades públicas) y, en su lugar, exponer los métodos de "hacer" cuya implementación es opaca. Otros han cubierto esto con los enlaces al programador pragmático.

Esta regla está relacionada con la regla sobre evitar el código de "doble punto" o "doble flecha", a menudo denominado "Solo hablar con amigos inmediatos", que dice que foo->getBar()->doSomething()es malo, en su lugar, use foo->doSomething();una llamada envolvente alrededor de la funcionalidad de la barra, y implementado de manera simple return bar->doSomething();: si fooes responsable de la gestión bar, ¡déjelo hacerlo!

Nicholas Shanks
fuente
1

Además de las otras buenas respuestas sobre "decir, no preguntar", algunos comentarios sobre sus ejemplos específicos que podrían ayudar:

El consejo en C ++ es que debe preferir las funciones gratuitas en lugar de las funciones miembro, ya que aumentan la encapsulación. Ambos son idénticos semánticamente, entonces, ¿por qué preferir la opción que tiene acceso a más estado?

Esa elección no tiene acceso a más estado. Ambos usan la misma cantidad de estado para hacer su trabajo, pero el ejemplo "malo" requiere que el estado de la clase sea público para hacer su trabajo. Además, el comportamiento de esa clase en el ejemplo 'malo' se extiende a la función libre, lo que hace que sea más difícil de encontrar y más difícil de refactorizar.

¿Por qué es responsabilidad del usuario formatear una cadena de error no relacionada? ¿Qué sucede si quiero hacer algo además de imprimir 'Sin nombre de calle en el archivo' si no tiene calle? ¿Qué pasa si la calle se llama igual?

¿Por qué es responsabilidad de 'street_name' hacer 'get street name' y 'proporcionar mensaje de error'? Al menos en la versión 'buena', cada pieza tiene una responsabilidad. Aún así, no es un gran ejemplo.

Telastyn
fuente
2
Eso no es cierto. Supone que controlar el sobrecalentamiento es lo único sensato que se puede hacer con la temperatura. ¿Qué sucede si la clase está destinada a ser uno de varios monitores de temperatura, y un sistema debe tomar medidas diferentes dependiendo de muchos de sus resultados, por ejemplo? Cuando este comportamiento se puede limitar al comportamiento predefinido de una sola instancia, entonces seguro. De lo contrario, obviamente no se puede aplicar.
DeadMG
Claro, o si el termostato y las alarmas existieran en diferentes clases (como probablemente deberían).
Telastyn el
1
@DeadMG: El consejo general es hacer que las cosas sean privadas / protegidas hasta que necesite acceso a ellas. Si bien este ejemplo en particular es meh, eso no cuestiona la práctica estándar.
Guvante
Un ejemplo en un artículo sobre la práctica de ser 'meh' lo cuestiona un poco. Si esta práctica es estándar debido a sus grandes beneficios, ¿por qué tener problemas para encontrar un ejemplo adecuado?
Stijn de Witt
1

Estas respuestas son muy buenas, pero aquí hay otro ejemplo solo para enfatizar: tenga en cuenta que generalmente es una forma de evitar la duplicación. Por ejemplo, supongamos que tiene VARIOS lugares con un código como:

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

Eso significa que será mejor que tengas algo como esto:

Product product = productMgr.get(productUuid, currentUser)

Debido a que esa duplicación significa que la mayoría de los clientes de su interfaz usarían el nuevo método, en lugar de repetir la misma lógica aquí y allá. Le da a su delegado el trabajo que desea hacer, en lugar de pedir la información que necesita para hacerlo usted mismo.

antonio.fornie
fuente
0

Creo que esto es más cierto cuando se escribe un objeto de alto nivel, pero menos cierto cuando se baja al nivel más profundo, por ejemplo, la biblioteca de clases, ya que es imposible escribir cada método para satisfacer a todos los consumidores de la clase.

Por ejemplo # 2, creo que está demasiado simplificado. Si realmente vamos a implementar esto, SystemMonitor terminaría teniendo el código para acceso de hardware de bajo nivel y lógica para abstracción de alto nivel incrustada en la misma clase. Desafortunadamente, si estamos tratando de separar eso en dos clases, violaríamos el "Dile, no preguntes".

El ejemplo # 4 es más o menos el mismo: está incorporando la lógica de la interfaz de usuario en el nivel de datos. Ahora, si vamos a arreglar lo que el usuario quiere ver en caso de que no haya una dirección, tenemos que arreglar el objeto en el nivel de datos, y ¿qué pasa si dos proyectos usan este mismo objeto pero necesitan usar texto diferente para la dirección nula?

Estoy de acuerdo en que si podemos implementar "Tell, Don't ask" para todo, sería muy útil. ¡Yo mismo sería feliz si pudiera decir en lugar de preguntar (y hacerlo yo mismo) en la vida real! Sin embargo, al igual que en la vida real, la viabilidad de la solución está muy limitada a las clases de alto nivel.

tia
fuente