Skip to content
Menu
CDhistory
CDhistory

Auditoría de métodos en Ruby usando Module#prepend

Posted on diciembre 12, 2021 by admin

Ayer estaba haciendo una entrevista para un puesto de desarrollador de Ruby en una empresa aquí en Denver cuando mi entrevistador me planteó un buen reto de metaprogramación: me pidió que escribiera un auditor de métodos automatizado.

Describió un método de clase que tomaría el nombre de un método de instancia y generaría un rastro de papel automático y registrado cada vez que se llamara a ese método de destino. Algo así:

Eso produciría una salida que se vería así:

Se me ocurrió una solución que funcionaba pero que implicaba algunos hacks potencialmente peligrosos para Object#send. Antes de presentar mi solución, dije «esto es probablemente una mala idea, pero…» y tenía razón. Las buenas ideas rara vez empiezan así.

La solución que propuso implicaba el uso de la a menudo olvidada Module#prepend y pensé que era lo suficientemente genial como para justificar un artículo.

La estrategia era utilizar el comportamiento de Module#prepend para crear un método envolvente generado dinámicamente para Messenger#share.

En esa envoltura, tendremos un mensaje «Performing» que se dispara antes de que se ejecute el cuerpo del método, la ejecución del método en sí, y luego un mensaje final «Exiting» una vez que la ejecución del método se ha completado con éxito.

Pero para entender cómo usar Module#prepend para hacer esto, primero necesitamos una buena comprensión de cómo funciona la herencia de métodos en Ruby.

  • Cadenas de Ancestros
  • El propio módulo
  • Juntando todo
  • Conclusión

Cadenas de Ancestros

Cada objeto Ruby se encuentra al final de lo que se llama una Cadena de Ancestros. Es una lista de los objetos ancestros de los que hereda el objeto. Ruby utiliza esta cadena de ancestros para determinar qué versión de un método (si es que hay alguna) se ejecuta cuando el objeto recibe un mensaje.

Puedes ver el árbol de ancestros de cualquier objeto llamando a Module#ancestors. Por ejemplo, aquí está la cadena de ancestros de nuestra clase Messenger:

Cuando introducimos #include o #prepend un módulo en una clase, Ruby realiza cambios en la cadena de ancestros de esa clase, modificando qué métodos se encuentran y en qué orden. La gran diferencia entre #include y #prepend es dónde se hace ese cambio. Creemos un módulo llamado Auditable que (eventualmente) contendrá el código que hace nuestra magia de auditoría:

Si usamos Module#include para importar los métodos (actualmente inexistentes) de Auditable, Ruby meterá ese módulo como ancestro de nuestra clase Messenger. Aquí, compruébalo tú mismo:

Cuando llamemos a un método de Messenger, Ruby mirará lo que está definido en la clase Messenger y -si no encuentra un método que coincida con la llamada- subirá por la cadena de ancestros hasta Auditable para buscar de nuevo. Si no encuentra lo que busca en Auditable pasa a Object y así sucesivamente.

Eso es #include. Si en cambio utilizamos Module#prepend para importar el contenido del módulo, obtenemos un efecto totalmente diferente:

#prepend hace que Messenger sea un ancestro de Auditable. Espera. ¿Qué?

Esto podría parecer que Messenger es ahora una superclase de Auditable, pero eso no es exactamente lo que está sucediendo aquí. Las instancias de la clase Messenger siguen siendo instancias de la clase Messenger, pero ahora Ruby buscará los métodos a utilizar para Messenger en el módulo Auditable antes de buscarlos en la propia clase Messenger.

Y eso, amigos, es lo que vamos a aprovechar para construir este auditor: si creamos un método llamado Auditable#share, Ruby lo encontrará antes de encontrar Messenger#share. Podemos entonces utilizar una llamada super en Auditable#share para acceder (¡y ejecutar!) el método original definido en Messenger.

El propio módulo

En realidad no vamos a crear un método llamado Auditable#share. ¿Por qué no? Porque queremos que esto sea una utilidad flexible. Si codificamos el método Auditable#share, sólo podremos usarlo en métodos que implementen el método #share. O peor, tendríamos que reimplementar este mismo patrón de auditor para cada método que queramos auditar. No gracias.

Así que en su lugar, vamos a definir nuestro método de forma dinámica y el método de la clase audit_method para dispararlo:

Cuando se llama a audit_method en una clase implementadora, se crea un método en el módulo Auditable con el mismo nombre que el método a auditar. En nuestro caso, se creará un método llamado Auditable#share. Como hemos dicho, Ruby encontrará este método antes de encontrar el método original en Messenger porque estamos anteponiendo el módulo Auditable en la clase implementadora.

Eso significa que podemos usar una superllamada para llegar a la cadena de ancestros y ejecutar Messenger#send. Cuando lo hacemos, pasamos los argumentos que hemos recogido (*arguments) hacia arriba de la cadena también.

Una vez que hemos llamado al método original, imprimimos nuestro mensaje de salida y lo damos por terminado. Buen trabajo, chicos!

Juntando todo

Ahora es sólo cuestión de prependponer este módulo en nuestra clase Messenger y deberíamos estar listos para ir:

Y vaya si funciona:

Las implicaciones aquí son enormes para la auditoría, pero hay más en este truco. Puedes usar el prepending para cambiar el comportamiento de los objetos sin cambiar los objetos mismos. Esta es una manera adaptable y clara de crear componentes de orden superior en Ruby. Pruebas de rendimiento, manejo de errores. Puedes hacer mucho aquí.

Conclusión

Los módulos de Ruby son más complicados de lo que la mayoría de la gente piensa. No es tan simple como «volcar» el código de un lugar a otro, y la comprensión de esas diferencias desbloquea algunas herramientas realmente aseado para su cinturón de utilidad.

Usted puede haber notado que sólo hablé de Module#include y Module#prepend hoy, y que no toqué en Module#extend. Eso es porque funciona de manera muy diferente a sus primos. Pronto escribiré una explicación en profundidad de Module#extend para completar el conjunto.

Por ahora, si quieres aprender más te recomiendo que leas Ruby modules: Include vs Prepend vs Extend de Léonard Hetsch. Fue realmente útil para armar todo esto.

Deja una respuesta Cancelar la respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Entradas recientes

  • Acela está de vuelta: NYC o Boston por 99 dólares
  • Entrada OMIM – # 608363 – SÍNDROME DE DUPLICACIÓN DEL CROMOSOMA 22q11.2
  • Los padres de Kate Albrecht – Conoce más sobre su padre Chris Albrecht y su madre Annie Albrecht
  • Temple Fork Outfitters
  • Burr (novela)

Archivos

  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • junio 2021
  • mayo 2021
  • abril 2021
  • DeutschDeutsch
  • NederlandsNederlands
  • SvenskaSvenska
  • DanskDansk
  • EspañolEspañol
  • FrançaisFrançais
  • PortuguêsPortuguês
  • ItalianoItaliano
  • RomânăRomână
  • PolskiPolski
  • ČeštinaČeština
  • MagyarMagyar
  • SuomiSuomi
  • 日本語日本語
©2022 CDhistory | Powered by WordPress & Superb Themes