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 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 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.
fuente
Respuestas:
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.
http://pragprog.com/articles/tell-dont-ask
fuente
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
temperature
como una consulta, mañana, el cliente puedecheck_for_underheating
sin tener que cambiarSystemMonitor
. Este no es el caso cuando elSystemMonitor
implementocheck_for_overheating
se implementa solo. Por lo tanto, unaSystemMonitor
clase cuyo trabajo es generar una alarma cuando la temperatura es demasiado alta sigue esto, pero unaSystemMonitor
clase 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.
fuente
check_for_underheating
sin tener que cambiarSystemMonitor
". ¿En qué se diferencia el clienteSystemMonitor
en 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.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?
¿O tiene cada tipo de sistema que define su capacidad de calefacción?
EDITAR:
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.
fuente
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.
fuente
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 ".
fuente
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, usefoo->doSomething();
una llamada envolvente alrededor de la funcionalidad de la barra, y implementado de manera simplereturn bar->doSomething();
: sifoo
es responsable de la gestiónbar
, ¡déjelo hacerlo!fuente
Además de las otras buenas respuestas sobre "decir, no preguntar", algunos comentarios sobre sus ejemplos específicos que podrían ayudar:
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 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.
fuente
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:
Eso significa que será mejor que tengas algo como esto:
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.
fuente
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.
fuente