Monkey Patching en lenguajes de programación dinámicos: Un ejemplo de JavaScript

Javascript
Programación dinámica
Monkey Patching en lenguajes de programación dinámicos cover image

Introducción

Este artículo explorará los conceptos de lenguajes de programación dinámicos y estáticos, las principales diferencias entre los dos y lo que ofrece cada paradigma en términos de ventajas y desventajas. Esta exploración se centrará aún más en los lenguajes de programación dinámicos, en particular uno de los patrones esenciales que habilita: Monkey Patch, este patrón se mostrará con la ayuda de un ejemplo en JavaScript.

Lenguajes de programación dinámicos vs estáticos

Terminología

Para comprender lo que constituye un lenguaje dinámico o estático, debemos establecer una comprensión de algunos términos clave que se usan comúnmente en este contexto: Tiempo de compilación, Tiempo de ejecución y Comprobación de tipo.

Compile y Runtime son dos términos que corresponden a diferentes etapas en el ciclo de vida de un programa de computadora, comenzando con Compile time.

Tiempo de compilación

El tiempo de compilación es el primer paso en el ciclo de vida de un programa. Un desarrollador escribe código en un lenguaje de programación dado. La mayoría de las veces, la máquina no puede comprender el código escrito en un lenguaje de alto nivel, por lo que se utiliza un compilador dedicado para traducirlo a un formato intermedio de nivel inferior que queda listo para la ejecución.

Tiempo de ejecución

El tiempo de ejecución generalmente encapsula dos pasos: cargar el programa en la memoria asignando los recursos necesarios para su ejecución junto con sus instrucciones y luego ejecutar el programa siguiendo el orden de esas instrucciones.

El siguiente diagrama ilustra este proceso:

Comprobación de tipo

La verificación de tipos es una función integrada en casi todos los lenguajes de programación. Es la capacidad de comprobar si un valor asignado a una determinada variable corresponde al tipo correcto de esa variable. Cada lenguaje de programación tiene una forma diferente de representar un valor de un tipo determinado en la memoria. Estas diferentes representaciones permiten comprobar la correspondencia entre el tipo de un valor y el tipo de una variable a la que se intenta asignar ese valor.

Ahora que tenemos una comprensión de alto nivel del ciclo de vida de un programa y la verificación de tipos, podemos proceder a explorar los lenguajes de programación estáticos.

Lenguajes de programación estáticos

Los lenguajes de programación estáticos, también conocidos como lenguajes tipificados estáticamente, son lenguajes que aplican la verificación de tipos que mencionamos en la fase de compilación. Esto significa efectivamente que una variable mantiene su tipo de declaración y no se le puede asignar ningún valor que no sean los valores de su tipo de declaración. Los lenguajes de programación estáticos ofrecen seguridad adicional cuando se trata de tipos, pero pueden ralentizar el proceso de desarrollo en ciertos casos de uso cuando esto se convierte en una restricción severa.

Lenguajes de programación dinámicos

Los lenguajes de programación dinámicos, por otro lado, aplican la verificación de tipos en tiempo de ejecución. Esto significa que cualquier variable puede contener cualquier valor en cualquier punto del programa. Esto puede ser beneficioso ya que ofrece un nivel de flexibilidad al desarrollador que no está presente en los lenguajes estáticos. Los lenguajes dinámicos tienden a ser más lentos en la ejecución que sus contrapartes estáticas, ya que implican un paso adicional de determinar dinámicamente la tipificación de cada variable.

Parche de mono

La escritura estática frente a la dinámica es un rasgo fundamental en un lenguaje de programación, ir con un paradigma sobre el otro puede permitir una gran cantidad de patrones y prácticas diferentes que pueden mejorar significativamente la calidad y la velocidad del desarrollo. También puede abrir la puerta a muchas limitaciones y antipatrones si no se toman en cuenta las consideraciones cuidadosas al tomar decisiones de diseño.

En particular, se sabe que los lenguajes de programación tipificados dinámicamente ofrecen un mayor nivel de flexibilidad, ya que no restringen una variable a un solo tipo. Esta flexibilidad conlleva el costo de una responsabilidad adicional para el desarrollador al implementar y depurar programas para asegurarse de que no ocurran comportamientos impredecibles. El patrón de parche de mono proviene de esta filosofía.

Monkey Patch se refiere al proceso de ampliar/cambiar el funcionamiento de un componente en tiempo de ejecución. El componente en cuestión puede ser una biblioteca, una clase, un método o incluso un módulo. La idea es la misma: se crea una pieza de código para realizar una determinada tarea, y el objetivo de Monkey Patching es cambiar o ampliar el comportamiento de esa pieza de código para que realice una nueva tarea, todo sin cambiar el código en sí.

Esto es posible en el lenguaje de programación dinámico ya que no importa con qué tipo de componente estemos tratando, todavía tiene la misma estructura de un objeto con diferentes atributos, los atributos pueden contener métodos que pueden reasignarse para lograr un nuevo comportamiento en el objeto sin entrar en sus aspectos internos y detalles de implementación. Esto se vuelve particularmente útil en el caso de bibliotecas y módulos de terceros, ya que tienden a ser más difíciles de modificar.

El siguiente ejemplo mostrará un caso de uso común que puede beneficiarse del uso de la técnica del parche de mono. Javascript se usó por el bien de la implementación aquí, pero esto aún debería aplicarse ampliamente a cualquier otro lenguaje de programación dinámico.

Ejemplo

Implemente un marco de prueba mínimo con el módulo HTTP nativo de Node

Las pruebas unitarias y de integración pueden incluirse en los casos de uso de la aplicación de parches Monkey. Por lo general, involucran casos de prueba que abarcan más de un servicio para pruebas de integración, o dependencias de API y/o bases de datos para pruebas unitarias. En estos dos escenarios, y para lograr los objetivos de las pruebas en primer lugar, nos gustaría que nuestras pruebas fueran independientes de estos recursos externos. La forma de lograr esto es a través de la burla. La simulación simula el comportamiento de los servicios externos para que la prueba pueda centrarse en la lógica real del código. Monkey patching puede ser útil aquí, ya que puede modificar los métodos de los servicios externos reemplazándolos con métodos de marcador de posición que llamamos "stub". Estos métodos devuelven el resultado esperado en los casos de prueba para que podamos evitar iniciar solicitudes a los servicios de producción solo por el bien de las pruebas.

El siguiente ejemplo es una implementación simple de parches de Monkey en el módulo http nativo de NodeJs. El módulo http es la interfaz que implementa los métodos del protocolo http para NodeJs. Se utiliza principalmente para crear servidores http barebone y comunicarse con servicios externos mediante el protocolo http.

En el siguiente ejemplo, tenemos un caso de prueba simple en el que llamamos a un servicio externo para obtener la lista de ID de usuario. En lugar de llamar al servicio real, parcheamos el método http get para que solo devuelva el resultado esperado, que es una matriz de identificaciones de usuario aleatorias. Esto puede no parecer de gran importancia ya que solo estamos obteniendo datos, pero si implementamos otro caso de prueba que implique alterar datos de algún tipo, podríamos alterar accidentalmente los datos en producción al ejecutar las pruebas.

De esta manera, podemos implementar nuestras funcionalidades y escribir pruebas para cada funcionalidad mientras garantizamos la seguridad de nuestros servicios de producción.

// import the http module
let http = require("http");

// patch the get method of the http module
http.get = async function(url) {
  return {
    data: ["1234", "1235", "1236", "1236"]
  };
}

// example test suite, call new patched get method for testing
test('get array of user ids from users api', async () => {
  const res = await http.get("https://users.api.com/ids");
  const userIds = res.data;
  expect(userIds).toBeDefined();
  expect(userIds.length).toBe(4);
  expect(userIds[0]).toBe("1234");
});

The code above is straightforward, we import the http module, reassign the http.get method with a new method that just returns an array of ids. Now we call the new patched method inside the test case and we get the new expected result.

~/SphericalTartWorker$ npm test

> nodejs@1.0.0 test
> jest

PASS  ./index.test.js
  ✓ get array of user ids from users api (25 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.977 s, estimated 2 s
Ran all test suites.

Common Pitfalls and Limitations

It should come as no surprise that monkey patching has its own flaws and limitations. In the context of modules in the node module system, patching a global module such as http is considered an operation with side effects, this is because http is accessible from any point inside the codebase and any other entity might have a dependency upon it. These entities expect the http module to operate in its usual behavior, by changing one of the http methods we effectively break all other http dependencies inside the codebase.

Since we are operating within a dynamically typed language, things might not fail immediately and would rather default to an unpredictable behavior which makes debugging an extremely complex task. In other use cases, there might be two different patches of the same component on the same attribute, in which case we cannot really predict which patch will take precedence over the other resulting in an even more unpredictable code.

It is also important to mention that monkey patching might have slight variations in behavior between different programming languages. It all depends on the language design and choices of implementation. For example, in python, not all instances using a patched method will be affected by the patch. If an instance explicitly calls the patched method then it will get the new updated version, on the contrary, other instances that might only have attributes pointing to the patched method and not explicitly calling it will get the original version, this is due to how python binding in classes operates.

Conclusion

In this article we explored the high-level distinctions between static and dynamic programming languages, we saw how dynamic programming languages can benefit from new paradigms and patterns leveraging the inherent flexibility these languages offer. The example we showcased was related to Monkey patching, a technique used to extend the behavior of code without changing it from the source. We saw a case where the use of this technique would be beneficial along with its potential drawbacks. Software development is all about tradeoffs, and employing the right solution for the problem requires elaborate considerations from the developer and a good understanding of architecture principles and fundamentals.


Career Services background pattern

Servicios profesionales

Contact Section background image

Mantengámonos en contacto

Code Labs Academy © 2024 Todos los derechos reservados.