动态编程语言中的猴子修补:一个 JavaScript 示例

JavaScript、动态规划
动态编程语言中的猴子修补 cover image

## 介绍

本文将探讨动态和静态编程语言的概念、两者之间的主要区别以及每种范例的优点和缺点。本次探索将进一步关注动态编程语言,特别是它所支持的基本模式之一:Monkey Patch,该模式将借助 JavaScript 中的示例进行展示。

动态与静态编程语言

术语

为了理解动态语言或静态语言的构成,我们需要了解在此上下文中常用的几个关键术语:编译时运行时和**类型检查* *。

编译和运行时是两个术语,对应于计算机程序生命周期的不同阶段(从编译时开始)。

编译时间

编译时是程序生命周期的第一步。开发人员用给定的编程语言编写代码。通常,机器无法理解用高级语言编写的代码,因此使用专用编译器将其转换为可供执行的较低级中间格式。

运行时

运行时通常封装两个步骤:通过分配程序执行所需的资源及其指令将程序加载到内存中,然后按照这些指令的顺序执行程序。

下图说明了这个过程:

类型检查

类型检查是几乎所有编程语言的内置功能。它能够检查分配给给定变量的值是否对应于该变量的正确类型。每种编程语言都有不同的方式来表示内存中给定类型的值。这些不同的表示形式使检查值的类型与您尝试分配该值的变量的类型之间的对应关系成为可能。

现在我们对程序生命周期和类型检查有了深入的了解,我们可以继续探索静态编程语言。

静态编程语言

静态编程语言,也称为静态类型语言,是在编译阶段应用我们提到的类型检查的语言。这实际上意味着变量在声明时保留其类型,并且除了其声明类型中的值之外,不能为其分配任何值。静态编程语言在处理类型时提供了额外的安全性,但在某些用例中,当这成为严格的限制时,可能会减慢开发过程。

动态编程语言

另一方面,动态编程语言在运行时应用类型检查。这意味着任何变量都可以在程序中的任何点保存任何值。这可能是有益的,因为它为开发人员提供了静态语言中不存在的一定程度的灵活性。动态语言的执行速度往往比静态语言慢,因为它们涉及动态确定每个变量的类型的额外步骤。

猴子补丁

静态类型与动态类型是编程语言的一个基本特征,使用一种范例而不是另一种范例可以实现许多不同的模式和实践,从而显着提高开发的质量和速度。如果在做出设计决策时没有仔细考虑,它还可能为许多限制和反模式打开大门。

特别是,众所周知,动态类型编程语言可以提供更高级别的灵活性,因为它们不将变量限制为单一类型。这种灵活性伴随着开发人员在实现和调试程序时承担额外责任的成本,以确保不会发生不可预测的行为。猴子补丁图案就源于这一理念。

猴子补丁是指在运行时扩展/更改组件工作的过程。所讨论的组件可以是库、类、方法甚至模块。想法是相同的:一段代码是为了完成某个任务而编写的,而猴子修补的目标是更改或扩展该代码段的行为,以便它完成新的任务,而所有这些都无需更改代码本身。

这在动态编程语言中是可能的,因为无论我们处理什么类型的组件,它仍然具有具有不同属性的对象的相同结构,属性可以保存可以重新分配的方法以在对象中实现新的行为而不深入其内部结构和实施细节。这在第三方库和模块的情况下变得特别有用,因为它们往往更难调整。

以下示例将展示可以从使用猴子补丁技术中受益的常见用例。这里使用 Javascript 是为了实现,但这仍然应该广泛适用于任何其他动态编程语言。

## 例子

使用 Node 的本机 HTTP 模块实现最小测试框架

单元和集成测试可以属于 Monkey 修补的用例。它们通常涉及跨多个服务进行集成测试的测试用例,或跨 API 和/或数据库依赖项进行单元测试的测试用例。在这两种情况下,为了首先实现测试目标,我们希望我们的测试独立于这些外部资源。实现这一目标的方法是通过模拟。模拟是模拟外部服务的行为,以便测试可以专注于代码的实际逻辑。猴子修补在这里很有用,因为它可以通过用我们称为“存根”的占位符方法替换外部服务的方法来修改它们。这些方法在测试用例中返回预期结果,因此我们可以避免仅仅为了测试而向生产服务发起请求。

以下示例是 NodeJs 原生 http 模块上 Monkey 修补的简单实现。 http 模块是为 NodeJs 实现 http 协议方法的接口。它主要用于创建准系统http服务器并使用http协议与外部服务进行通信。

在下面的示例中,我们有一个简单的测试用例,我们调用外部服务来获取用户 ID 列表。我们不调用实际的服务,而是修补 http get 方法,以便它只返回预期的结果,即随机用户 ID 的数组。这似乎并不重要,因为我们只是获取数据,但如果我们实现另一个涉及更改某种数据的测试用例,我们可能会在运行测试时意外地更改生产数据。

这样我们就可以实现我们的功能,并为每个功能编写测试,同时确保生产服务的安全。

// 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");
});

上面的代码很简单,我们导入 http 模块,使用仅返回 ids 数组的新方法重新分配 http.get 方法。现在我们在测试用例中调用新的修补方法,并得到新的预期结果。

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

常见陷阱和限制

猴子补丁有其自身的缺陷和局限性,这一点不足为奇。在节点模块系统中的模块上下文中,修补全局模块(例如 http)被认为是具有副作用的操作,这是因为可以从代码库内的任何点访问 http,并且任何其他实体都可能依赖于它。这些实体期望 http 模块以其通常的行为运行,通过更改 http 方法之一,我们有效地破坏了代码库内的所有其他 http 依赖项。

由于我们使用动态类型语言进行操作,事情可能不会立即失败,而是默认为不可预测的行为,这使得调试成为一项极其复杂的任务。在其他用例中,同一组件的同一属性可能有两个不同的补丁,在这种情况下,我们无法真正预测哪个补丁将优先于另一个补丁,从而导致代码更加不可预测。

值得一提的是,猴子补丁在不同编程语言之间的行为可能会略有不同。这一切都取决于语言设计和实现的选择。例如,在 python 中,并非所有使用修补方法的实例都会受到修补程序的影响。如果一个实例显式调用修补方法,那么它将获得新的更新版本,相反,其他可能只有属性指向修补方法并且没有显式调用它的实例将获得原始版本,这是由于 python类中的绑定起作用。

## 结论

在本文中,我们探讨了静态编程语言和动态编程语言之间的高级区别,我们了解了动态编程语言如何利用这些语言提供的固有灵活性从新范例和模式中受益。我们展示的示例与 Monkey 修补相关,这是一种用于扩展代码行为而不从源代码更改代码的技术。我们看到了一个案例,使用这种技术既有好处,也有潜在的缺点。软件开发都是关于权衡的,为问题采用正确的解决方案需要开发人员仔细考虑以及对架构原理和基础知识的充分理解。


Career Services background pattern

职业服务

Contact Section background image

让我们保持联系

Code Labs Academy © 2024 版权所有.