ruby on rails - Pros y contras de usar devoluciones de llamada para la lógica de dominio en Rails
default value mongoid (6)
¿Qué ve como los pros y los contras de usar devoluciones de llamada para la lógica de dominio? (Estoy hablando en el contexto de Rails y / o proyectos de Ruby).
Para comenzar la discusión, quería mencionar esta cita de la página de Mongoid en devoluciones de llamada :
El uso de devoluciones de llamada para la lógica del dominio es una mala práctica de diseño y puede provocar errores inesperados que son difíciles de depurar cuando las devoluciones de llamada en la cadena detienen la ejecución. Es nuestra recomendación utilizarlos solo para asuntos transversales, como poner en cola los trabajos en segundo plano.
Me interesaría escuchar el argumento o la defensa detrás de esta afirmación. ¿Está previsto que se aplique solo a las aplicaciones respaldadas por Mongo? ¿O está destinado a aplicarse a través de tecnologías de base de datos?
Parecería que la Guía Ruby on Rails para las validaciones de ActiveRecord y las devoluciones de llamada puede estar en desacuerdo, al menos cuando se trata de bases de datos relacionales. Tomemos este ejemplo:
class Order < ActiveRecord::Base
before_save :normalize_card_number, :if => :paid_with_card?
end
En mi opinión, este es un ejemplo perfecto de una devolución de llamada simple que implementa la lógica del dominio. Parece rápido y eficaz. Si tuviera que seguir el consejo de Mongoid, ¿a dónde iría esta lógica?
Avdi Grimm tiene algunos grandes ejemplos en su libro Object on Rails .
Encontrará here y here por qué no elige la opción de devolución de llamada y cómo puede deshacerse de esto simplemente anulando el método ActiveRecord correspondiente.
En tu caso terminarás con algo como:
class Order < ActiveRecord::Base
def save(*)
normalize_card_number if paid_with_card?
super
end
private
def normalize_card_number
#do something and assign self.card_number = "XXX"
end
end
[ACTUALIZACIÓN después de su comentario "esto sigue siendo devolución de llamada"]
Cuando hablamos de devoluciones de llamada para lógica de dominio, entiendo las devoluciones de llamada de ActiveRecord
, corríjame si cree que la cita del referente de Mongoid a otra cosa, si hay un "diseño de devolución de llamada" en algún lugar no lo encontré.
Creo que las devoluciones de llamada de ActiveRecord
son, en su mayor parte, nada más que el azúcar sintáctico que puede eliminar con mi ejemplo anterior.
Primero, estoy de acuerdo en que este método de devolución de llamadas oculta la lógica detrás de ellos: para alguien que no esté familiarizado con ActiveRecord
, tendrá que aprenderlo para entender el código, con la versión anterior, es fácilmente comprensible y verificable.
Lo que podría ser peor con las devoluciones de llamada de ActiveRecord
su "uso común" o la "sensación de desacoplamiento" que pueden producir. La versión de devolución de llamada puede parecer agradable al principio, pero a medida que agregue más devoluciones de llamada, será más difícil entender su código (en qué orden se cargan, cuál puede detener el flujo de ejecución, etc.) y probarlo ( su lógica de dominio está acoplada con la lógica de persistencia de ActiveRecord
).
Cuando leo mi ejemplo a continuación, me siento mal por este código, me huele. Creo que probablemente no termines con este código si estuvieras haciendo TDD / BDD y, si te olvidas de ActiveRecord
, creo que simplemente card_number=
escrito el método card_number=
. Espero que este ejemplo sea lo suficientemente bueno como para no elegir directamente la opción de devolución de llamada y pensar primero en el diseño.
Acerca de la cita de MongoId Me pregunto por qué aconsejan no usar la devolución de llamada para la lógica del dominio, sino usarla para poner en cola el trabajo en segundo plano. Creo que el trabajo de fondo en cola podría ser parte de la lógica del dominio y, a veces, podría estar mejor diseñado con algo más que una devolución de llamada (digamos un observador).
Finalmente, hay algunas críticas sobre cómo se utiliza / implementa ActiveRecord con Rail desde un punto de vista de diseño de programación orientado a objetos, esta answer contiene buena información sobre el mismo y la encontrará más fácilmente. Es posible que también desee verificar el proyecto de implementación del patrón de datos / ruby, que podría ser un reemplazo (pero cuánto mejor) para ActiveRecord y no tiene su debilidad.
En mi opinión, el mejor escenario para el uso de devoluciones de llamada es cuando el método de activación no tiene nada que ver con lo que se ejecuta en la devolución de llamada en sí. Por ejemplo, un buen before_save :do_something
no debe ejecutar código relacionado con guardar . Es más como cómo debería trabajar un observador .
Las personas tienden a usar devoluciones de llamada solo para SECAR su código. No está mal, pero puede llevar a un código complicado y difícil de mantener, porque leer el método de save
no le dice todo lo que hace si no nota que se llama a una devolución de llamada. Creo que es importante el código explícito (especialmente en Ruby y Rails, donde ocurre tanta magia).
Todo lo relacionado con el ahorro debe estar en el método de save
. Si, por ejemplo, la devolución de llamada es para asegurarse de que el usuario está autenticado, lo que no tiene relación con el ahorro , entonces es un buen escenario de devolución de llamada.
Esta pregunta aquí ( ignorar los fallos de validación en rspec ) es una excelente razón para no poner lógica en sus devoluciones de llamada: Probabilidad.
Su código puede tener una tendencia a desarrollar muchas dependencias en el tiempo, donde comienza a agregar a unless Rails.test?
en sus métodos.
Recomiendo solo mantener la lógica de formato en su before_validation
llamada de before_validation
y mover las cosas que afectan a varias clases a un objeto Servicio.
Por lo tanto, en su caso, movería el número de tarjeta normalizar a una validación antes de, y luego puede validar que el número de la tarjeta está normalizado.
Pero si necesita salir y crear un perfil de pago en algún lugar, lo haría en otro objeto de flujo de trabajo de servicio:
class CreatesCustomer
def create(new_customer_object)
return new_customer_object unless new_customer_object.valid?
ActiveRecord::Base.transaction do
new_customer_object.save!
PaymentProfile.create!(new_customer_object)
end
new_customer_object
end
end
A continuación, puede probar fácilmente ciertas condiciones, como si no es válida, si el guardado no se realiza o si la pasarela de pago produce una excepción.
No creo que la respuesta sea demasiado complicada.
Si tiene la intención de crear un sistema con un comportamiento determinista, las devoluciones de llamadas que tratan con datos relacionados con la información, como la normalización, están bien, las devoluciones de llamadas relacionadas con la lógica empresarial, como el envío de correos electrónicos de confirmación, no están bien .
La OOP se popularizó con el comportamiento emergente como una buena práctica 1 , y según mi experiencia, Rails parece estar de acuerdo. Muchas personas, 1 , piensan que esto causa un dolor innecesario para las aplicaciones en las que el comportamiento en tiempo de ejecución es determinista y bien conocido de antemano.
Si está de acuerdo con la práctica del comportamiento emergente OO, entonces el patrón de registro activo del comportamiento de acoplamiento al gráfico de objetos de datos no es tan importante. Si (como yo) ve / ha sentido el dolor de comprender, depurar y modificar tales sistemas emergentes, querrá hacer todo lo posible para que el comportamiento sea más determinista.
Ahora, ¿cómo se diseñan los sistemas OO con el balance correcto de acoplamiento suelto y comportamiento determinista? Si conoces la respuesta, escribe un libro, ¡lo compraré! 1 , diseño impulsado por dominio , y más generalmente los patrones GoF son un comienzo :-)
- 1 , "¿En qué nos equivocamos?". No es una fuente primaria, pero es consistente con mi comprensión general y experiencia subjetiva de suposiciones en el mundo salvaje.
Realmente me gusta usar callbacks para clases pequeñas. Creo que hace que una clase sea muy legible, por ejemplo, algo como
before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache
De inmediato queda claro lo que está sucediendo.
Incluso encuentro esto comprobable; Puedo probar que los métodos en sí funcionan y puedo probar cada devolución de llamada por separado.
Creo firmemente que las devoluciones de llamada en una clase solo deben usarse para aspectos que pertenecen a la clase. Si desea desencadenar eventos al guardar, por ejemplo, enviar un correo si un objeto está en un estado determinado, o iniciar sesión, usaría un Observer . Esto respeta el principio de responsabilidad única.
Devoluciones de llamada
La ventaja de las devoluciones de llamada:
- todo está en un solo lugar, por lo que es fácil
- código muy legible
La desventaja de las devoluciones de llamada:
- Dado que todo es un lugar, es fácil romper el principio de responsabilidad única
- podria hacer para clases pesadas
- ¿Qué pasa si falla una devolución de llamada? ¿Todavía sigue la cadena? Sugerencia: asegúrese de que sus devoluciones de llamada nunca fallan, o establezca de otra manera el estado del modelo como no válido.
Observadores
La ventaja de los observadores
- código muy limpio, puedes hacer varios observadores para la misma clase, cada uno haciendo una cosa diferente
- La ejecución de observadores no está acoplada.
La desventaja de los observadores.
- al principio podría ser extraño cómo se desencadena el comportamiento (¡mire en el observador!)
Conclusión
Así que en resumen:
- use devoluciones de llamada para las cosas simples relacionadas con el modelo (valores calculados, valores predeterminados, validaciones)
- utilizar observadores para un comportamiento más transversal (por ejemplo, envío de correo, estado de propagación, ...)
Y como siempre: todos los consejos deben tomarse con un grano de sal. Pero en mi experiencia, los observadores escalan realmente bien (y también son poco conocidos).
Espero que esto ayude.
EDITAR: He combinado mis respuestas en las recomendaciones de algunas personas aquí.
Resumen
Basándome en algunas lecturas y pensamientos, he llegado a algunas declaraciones (tentativas) de lo que creo:
La declaración "El uso de devoluciones de llamada para la lógica del dominio es una mala práctica de diseño" es falso, como está escrito. Se sobreestima el punto. Las devoluciones de llamada pueden ser un buen lugar para la lógica de dominio, si se utiliza de forma adecuada. La pregunta no debería ser si la lógica del modelo de dominio debe ir en devoluciones de llamada, es qué tipo de lógica de dominio tiene sentido entrar.
La declaración "El uso de devoluciones de llamada para la lógica del dominio ... puede dar lugar a errores inesperados que son difíciles de depurar cuando las devoluciones de llamada en la cadena detienen la ejecución" es cierto.
Sí, las devoluciones de llamada pueden provocar reacciones en cadena que afectan a otros objetos. En la medida en que esto no sea verificable, esto es un problema.
Sí, debería poder probar la lógica de su negocio sin tener que guardar un objeto en la base de datos.
Si las devoluciones de llamada de un objeto se hinchan demasiado para su sensibilidad, hay diseños alternativos a considerar, que incluyen (a) observadores o (b) clases de ayuda. Estos pueden manejar limpiamente múltiples operaciones de objetos.
El consejo de "usar solo [devoluciones de llamada] para asuntos transversales, como poner en cola los trabajos en segundo plano" es interesante pero exagerado. (Revisé las preocupaciones transversales para ver si quizás estaba pasando por alto algo).
También quiero compartir algunas de mis reacciones a las publicaciones del blog que he leído que hablan sobre este tema:
Reacciones a las "devoluciones de llamada de ActiveRecord arruinaron mi vida"
La publicación de Mathias Meyer en 2010, las llamadas devueltas de llamada de ActiveRecord arruinaron mi vida , ofrece una perspectiva. El escribe:
Cada vez que comencé a agregar validaciones y devoluciones de llamada a un modelo en una aplicación de Rails, [...] simplemente me sentí mal. Sentí como si estuviera agregando un código que no debería estar allí, eso hace que todo sea mucho más complicado y se convierte en explícito en un código implícito.
Considero que esta última afirmación "se convierte en explícita en código implícito" es, bueno, una expectativa injusta. Estamos hablando de Rails aquí, ¿verdad? Gran parte del valor agregado tiene que ver con que Rails haga las cosas "mágicamente", por ejemplo, sin que el desarrollador tenga que hacerlo explícitamente. ¿No parece extraño disfrutar de los frutos de Rails y, sin embargo, criticar el código implícito?
Código que solo se ejecuta en función del estado de persistencia de un objeto.
Estoy de acuerdo en que esto suena desagradable.
Código que es difícil de probar, porque necesita guardar un objeto para probar partes de su lógica empresarial.
Sí, esto hace que las pruebas sean lentas y difíciles.
Entonces, en resumen, creo que Mathias agrega un combustible interesante al fuego, aunque no lo encuentro todo convincente.
Reacciones a "Loco, herético e impresionante: la forma en que escribo aplicaciones de Rails"
En la publicación de James Golick de 2010, Crazy, Heretical, and Awesome: The Way I Write Rails Apps , escribe:
Además, unir toda la lógica de su negocio a sus objetos de persistencia puede tener efectos secundarios extraños. En nuestra aplicación, cuando se crea algo, una devolución de llamada after_create genera una entrada en los registros, que se utilizan para producir la fuente de actividad. ¿Qué sucede si deseo crear un objeto sin iniciar sesión, por ejemplo, en la consola? No puedo El ahorro y el registro están casados por siempre y por toda la eternidad.
Más tarde, llega a la raíz de ello:
La solución es en realidad bastante simple. Una explicación simplificada del problema es que violamos el principio de responsabilidad única. Por lo tanto, vamos a utilizar técnicas estándar orientadas a objetos para separar las preocupaciones de nuestra lógica modelo.
Realmente aprecio que modere su consejo diciéndole cuándo se aplica y cuándo no:
La verdad es que, en una aplicación simple, los objetos de persistencia obesos nunca pueden doler. Es cuando las cosas se vuelven un poco más complicadas que las operaciones de CRUD que estas cosas comienzan a acumularse y se convierten en puntos de dolor.