Monkey Patching in Dynamic Programming Languages: A JavaScript Example

Javascript
Dynamic Programming
Monkey Patching in Dynamic Programming Languages cover image

Introduction

This article will explore the concepts of Dynamic and Static programming languages, the main differences between the two, and what each paradigm provides in terms of advantages and pitfalls. This exploration will further focus on dynamic programming languages, particularly one of the essential patterns it enables: Monkey Patch, this pattern will be showcased with the help of an example in JavaScript.

Dynamic vs Static Programming Languages

Terminology

In order to understand what constitutes a dynamic language or a static one, we need to establish an understanding of a few key terms commonly used in this context: Compile time, Runtime, and Type checking.

Compile and Runtime are two terms that correspond to different stages in the lifecycle of a computer program, starting with Compile time.

Compile Time

Compile time is the first step in the lifecycle of a program. A developer writes code in a given programming language. More often than not, the machine is unable to understand the code written in a high-level language so a dedicated compiler is used to translate it to a lower-level intermediate format that becomes ready for execution.

Runtime

Runtime usually encapsulates two steps: loading the program into memory by allocating the resources needed for its execution along with its instructions, and then executing the program following the order of those instructions.

The following diagram illustrates this process:

Type Checking

Type-checking is a built-in feature in almost all programming languages. It is the ability to check if a value assigned to a given variable corresponds to the correct type of that variable. Each programming language has a different way to represent a value of a given type in memory. These different representations make it possible to check the correspondence between the type of a value and the type of a variable you attempt to assign that value to.

Now that we have a high-level understanding of a program lifecycle and type checking, we can proceed to explore static programming languages.

Static Programming Languages

Static Programming Languages, also referred to as statically typed languages are languages that apply the type checking we mentioned at the compile phase. This effectively means that a variable keeps its type from declaration and no value can be assigned to it other than values from its declaration type. Static programming languages offer extra safety when dealing with types but can slow down the development process in certain use-cases when this becomes a harsh restriction.

Dynamic Programming Languages

Dynamic programming languages, on the other hand, apply type-checking at runtime. This means any variable can hold any value at any point in the program. This can be beneficial as it offers a level of flexibility to the developer that is not present in the static languages. Dynamic languages tend to be slower at execution than their static counterparts since they involve an additional step of dynamically figuring out the typing of each variable.

Monkey Patch

Static vs Dynamic Typing is a fundamental trait in a programming language, going with one paradigm over the other can enable a host of different patterns and practices that can significantly improve the quality and the speed of development. It can also open the door for many limitations and anti-patterns if no careful considerations are given when making design decisions.

Particularly, dynamically typed programming languages are known to offer a higher level of flexibility since they don't restrict a variable to a single type. This flexibility comes with the cost of additional responsibility to the developer when implementing and debugging programs to make sure no unpredictable behaviors are occurring. The monkey patch pattern comes from this philosophy.

Monkey Patch refers to the process of extending/changing the workings of a component at runtime. The component in question can be a library, a class, a method, or even a module. The idea is the same: a piece of code is made to accomplish a certain task, and the goal of monkey patching is to change or extend the behavior of that piece of code so that it accomplishes a new task, all without changing the code itself.

This is made possible in dynamic programming language since no matter what type of component we are dealing with, it still has the same structure of an object with different attributes, the attributes can hold methods that can be reassigned to achieve a new behavior in the object without going into its internals and details of implementation. This becomes particularly useful in case of third party libraries and modules as those tend to be harder to tweak.

The following example will showcase a common use case that can benefit from using the monkey patch technique. Javascript was used for the sake of implementation here but this should still broadly apply to any other dynamic programming language.

Example

Implement A Minimal Testing Framework with Node’s Native HTTP Module

Unit and integration testing can fall under the use cases of Monkey patching. They usually involve test cases that span across more than one service for integration testing, or API and/or database dependencies for unit testing. In these two scenarios, and in order to accomplish the goals of testing in the first place we would want our tests to be independent of these external resources. The way to achieve this is through mocking. Mocking is simulating the behavior of external services so the test can focus on the actual logic of the code. Monkey patching can be helpful here since it can modify the methods of the external services by replacing them with placeholder methods that we call “stub”. These methods return the expected result in the testing cases so we can avoid initiating requests to production services just for the sake of tests.

The following example is a simple implementation of Monkey patching on the NodeJs native http module. The http module is the interface that implements the http protocol methods for NodeJs. It is mainly used to create barebone http servers and communicate with external services using the http protocol.

In the example below we have a simple testing case where we call an external service to fetch the list of user ids. Rather than calling the actual service we patch the http get method so it just returns the expected result which is an array of random user ids. This might not seem of great importance since we are just fetching data but if we implement another test case that involves altering data of some sort, we might accidentally alter data on production upon running tests.

This way we can implement our functionalities, and write tests for each functionality while assuring the safety of our production services.

// 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

Career Services

Contact Section background image

Let’s stay in touch

Code Labs Academy © 2024 All rights reserved.