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

47

Este blogpost se publicó en Hacker News con varios comentarios positivos. Viniendo de C ++, la mayoría de estos ejemplos parecen ir en contra de lo que me han enseñado.

Como el ejemplo # 2:

Malo:

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

contra bien:

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 debería preferir las funciones libres en lugar de las funciones miembro, ya que aumentan la encapsulación. Ambos son idénticos semánticamente, así que ¿por qué preferir la opción que tiene acceso a más estados?

Ejemplo 4:

Malo:

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

contra bien:

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 de User formatear 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 tiene el mismo nombre?

¿Podría alguien explicarme las ventajas y el razonamiento de "Decir, no preguntar"? No estoy buscando cuál es mejor, sino tratar de entender el punto de vista del autor.

    
pregunta Pubby 20.07.2012 - 02:30

9 respuestas

78

Preguntar al objeto sobre su estado, y luego llamar a métodos en ese objeto basándose en decisiones tomadas fuera del objeto, significa que el objeto ahora es una abstracción con fugas; parte de su comportamiento está ubicado fuera del objeto, y el estado interno está expuesto (tal vez innecesariamente) al mundo exterior.

  

Debes esforzarte por decirle a los objetos lo que quieres que hagan; no haga   Hágales preguntas sobre su estado, tome una decisión y luego diga   ellos qué hacer.

     

El problema es que, como persona que llama, no debes tomar decisiones   basado en el estado del objeto llamado que resulta en usted, entonces   Cambiando el estado del objeto. La lógica que estás implementando es   Probablemente la responsabilidad del objeto llamado, no la tuya. Para que tú   tomar decisiones fuera del objeto viola su encapsulación.

     

Claro, puedes decir, eso es obvio. Nunca escribiría un código así.   Sin embargo, es muy fácil dejarse llevar por el examen de algunas referencias   Objeto y luego llamando a diferentes métodos basados en los resultados. Pero   Esa puede no ser la mejor manera de hacerlo. Decirle al objeto   Lo que quieras. Déjalo descubrir cómo hacerlo. Pensar declarativamente   en lugar de procedimiento!

     

Es más fácil mantenerse al margen de esta trampa si comienzas diseñando   clases basadas en sus responsabilidades; entonces puedes progresar   naturalmente a especificar comandos que la clase puede ejecutar, como   a diferencia de las consultas que le informan sobre el estado del objeto.

enlace

    
respondido por el Robert Harvey 20.07.2012 - 02:48
16

En general, la pieza sugiere que no debería exponer el estado miembro para que otros razonen, si usted mismo podría razonar .

Sin embargo, lo que no está claramente establecido es que esta ley cae dentro de los límites obvios de muy cuando el razonamiento está muy por encima de 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 temperature como una consulta, mañana, el cliente puede check_for_underheating sin tener que cambiar SystemMonitor . Este no es el caso cuando el SystemMonitor implementa el mismo check_for_overheating . Por lo tanto, una clase SystemMonitor cuyo trabajo es dar una alarma cuando la temperatura es demasiado alta sigue esto, pero una clase SystemMonitor cuyo trabajo es permitir que otra pieza de código lea la temperatura para que pueda controlar, digamos , TurboBoost o algo así, no debería.

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

    
respondido por el DeadMG 20.07.2012 - 03:42
9

El problema real con su ejemplo de sobrecalentamiento es que las reglas para lo que califica como sobrecalentamiento no se modifican fácilmente para diferentes sistemas. Supongamos que el Sistema A es como lo tiene (la temperatura > 100 se está sobrecalentando) pero el Sistema B es más delicado (la temperatura > 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 es que cada tipo de sistema define su capacidad de calefacción?

EDIT:

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 forma anterior hace que su función de control se vuelva fea a medida que comienza a tratar con más sistemas. El último permite que la función de control sea estable a medida que pasa el tiempo.

    
respondido por el Matthew Flynn 20.07.2012 - 19:16
6

En primer lugar, creo que debo hacer una excepción a su caracterización de los ejemplos como "mala" y "buena". El artículo usa los términos "No tan bueno" y "Mejor", creo que esos términos se eligieron por una razón: 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 para incluir cualquier funcionalidad que dependa únicamente de la clase en la clase en lugar de fuera de ella. La razón es debido a encapsulación, y el hecho de que hace que sea más fácil evolucionar la clase a lo largo del tiempo. La clase también hace un mejor trabajo publicitando 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 haga la mayoría de los usuarios de la clase. A veces, quiere decirlo, porque el comportamiento es contrario a la intuición para la clase y no quiere 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 trata de Estado o Ciudad, es posible que desee un valor predeterminado al asignar un registro a un vendedor o encuestador, para que todas las incógnitas se dirijan a una persona específica. Por otro lado, si estuviera imprimiendo sobres, es posible que prefiera una excepción o una 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" sea el camino a seguir, pero en general, "Mejor" es, bueno, mejor.

    
respondido por el jmoreno 20.07.2012 - 05:18
3

Datos / Objeto anti-simetría

Como han señalado otros, Tell-Dont-Ask es específicamente para los casos en los que cambia el estado del objeto después de la consulta (consulte, por ejemplo, el texto Pragprog publicado en otra parte de esta página). Este no es siempre el caso, por ejemplo. el objeto 'usuario' no se 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 extraer lógica de una clase que debería estar justificada dentro de ella. Pero no toda la lógica que trata con los objetos es necesariamente la lógica de esos objetos. Esto está sugiriendo a 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 sean realmente contenedores de propiedades, quizás incluso inmutables, y luego ejecutar varias funciones en colecciones de dichos objetos, evaluándolos, filtrando o transformándolos en lugar de enviarles comandos. (que es más el dominio de Tell-Dont-Ask).

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

Bob Martin, en su libro "Clean Code", llama a esto "Datos / Objeto Anti-Simetría" (p.95ff), otras comunidades pueden referirse a él como " problema de expresión ".

    
respondido por el ThomasH 22.07.2014 - 15:12
3

Este paradigma a veces se denomina "Diga, no pregunte" , lo que significa decirle al objeto qué hacer, no pregunte sobre su estado; y, a veces, como "Preguntar, no decir" , es decir, pedirle al objeto que haga algo por usted, no decirle cuál debe ser su estado. En cualquier caso, 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 que llama. Las interfaces deben evitar exponer su estado (por ejemplo, a través de accesores o propiedades públicas) y, en cambio, 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 "punto doble" o "flecha doble", a menudo denominado "solo hablar con amigos inmediatos", que indica que foo->getBar()->doSomething() es incorrecto, en su lugar, use foo->doSomething(); , que es una llamada envolvente sobre la funcionalidad de bar, y se implementa simplemente como return bar->doSomething(); - si foo es responsable de administrar bar , ¡entonces déjelo hacerlo!

    
respondido por el Nicholas Shanks 24.02.2016 - 15:38
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 debería preferir las funciones libres en lugar de las funciones miembro, ya que aumentan la encapsulación. Ambos son idénticos semánticamente, así que ¿por qué preferir la opción que tiene acceso a más estados?

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

  

¿Por qué es responsabilidad del usuario dar formato a una cadena de error no relacionada? ¿Qué pasa si quiero hacer algo además de imprimir 'No hay nombre de calle en el archivo' si no tiene calle? ¿Qué pasa si la calle tiene el mismo nombre?

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

    
respondido por el Telastyn 20.07.2012 - 03:37
1

Estas respuestas son muy buenas, pero aquí hay otro ejemplo para enfatizar: tenga en cuenta que generalmente es una forma de evitar la duplicación. Por ejemplo, digamos 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 es mejor que tengas algo como esto:

Product product = productMgr.get(productUuid, currentUser)

Porque 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 que haga, en lugar de solicitar la información que necesita para hacerlo usted mismo.

    
respondido por el antonio.fornie 13.01.2016 - 17:35
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. biblioteca de clases, ya que es imposible escribir todos los métodos para satisfacer a todos los consumidores de la clase.

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

El ejemplo # 4 es más o menos el mismo: está incorporando la lógica de UI en el nivel de datos. Ahora, si vamos a arreglar lo que el usuario desea ver en caso de no tener 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 un texto diferente para la dirección nula?

Estoy de acuerdo en que si pudiéramos implementar "Diga, no pregunte" por 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.

    
respondido por el tia 20.07.2012 - 19:38

Lea otras preguntas en las etiquetas