最小化动态编程语言的缺点

e05a261a92fcf6c30ee4c851ed82f0f7.gif

本文介绍了动态语言的优缺点,倡导大家在享受动态语言的自由度时,也要尽可能地像静态语言那样思考,遵循一些规范和原则,避免一些隐患和错误。

原文链接:https://stackoverflow.blog/2023/01/19/adding-structure-to-dynamic-languages/

未经允许,禁止转载!

作者 | Daniel Orner      译者 | 明明如月

责编 | 夏萌

出品 | CSDN(ID:CSDNnews)

过去很长一段时间内,大多数软件开发依赖的都是静态类型和编译型语言,如 C、C++ 以及 Java。当动态语言开始崭露头角时,最初它们常常被贬低为“玩具”语言,而 JavaScript 就是其中之一。然而,随着时间的推移,解释型动态编程语言的优势开始得到认同。

现今,尤其在网络应用程序领域,使用动态语言的频率几乎与使用静态语言相当。JavaScript 和 Python 常年稳居常年最受欢迎语言前五之列,同时其他如 PHP 和 Ruby 等语言也始终占据一席之地。

动态编程语言带来了多项优势:

  • 它们是解释型的而非编译型的,无需在代码执行时额外的编译步骤和等待时间。

  • 代码一般更为简洁,因为不需要花费大量时间在定义和转换类型上。

  • 它们允许创建 领域特定语言 (DSLs),可以更自然地定义应用程序路由或配置。

  • 通常可以在运行时动态修改或“猴子补丁”(即“动态打补丁”),方便地为第三方库的对象添加新功能。

  • 可以容易地实现混合类型的集合,如数组和字典,这在静态类型语言中通常难以实现。

  • 类型转换通常更为简便——无需为不同的输入参数类型写多份函数,可以设计一个函数,接受任何类型的参数并进行必要的类型转换。

  • 元编程允许你轻松调整第三方代码,避免了漫长的分支、拉取请求和合并流程,这个过程可能会耗费数月甚至数年,取决于项目的规模。

你们中的细心人可能已经发现,这个列表中的每一项都既可以被视为优点,也可以作为缺点。让我们一一分析:

  • 解释器无法像编译器那样全面地捕获问题。

  • 不够精确的类型定义可能引发更多错误。

  • DSL(领域特定语言)可能引入混乱,增加你在理解应用和语言方面的心智负担。

  • 如果任何人都能修改一个类型,他们可能会滥用本应只在内部使用的代码。

  • 混合类型的集合的使用通常既不正确又混乱。

  • 一个函数应用于多个类型可能导致混乱。

  • 对第三方类进行动态打补丁可能导致代码过时和升级难。

随着动态语言越来越受欢迎,业界对如何解决这些问题进行了热议。我对动态语言的感知也在不断变化,最初对它们所带来的自由感到激动,但后来开始意识到其中潜在的问题。

我现在主要使用 Ruby,可能是最“动态”的流行动态语言之一。然而,即使在使用 Ruby 的过程中,我也发现了一套实践,我认为它们可以帮助缓解动态编程的一些缺点。

78a5ff28869202912a2406e17e3dcc97.png

充分发挥类型提示的作用

过去十年间,主流的动态编程语言都陆续引入了类型提示功能。其中最著名、使用最广泛的无疑是 TypeScript,它是 JavaScript 的超集,需要一个编译器将其代码转化为标准的 JavaScript。Python 和 PHP 在语言层面都引入了类型提示,即使是 Ruby 也试图进行了 RBS 和 Sorbet 项目(虽然这两个项目似乎并未得到广泛的支持)。

类型提示是使动态语言走向静态化的最直接路径。实际上,这让你能够享受最好的两个世界:编写动态代码的同时,需要更加谨慎地确定你期待获取和使用的数据类型。

有趣的是,优秀的类型提示系统往往比静态语言的类型系统更为复杂。以 Java 为例,你无法声明一个对象属性可以是整数 或者 字符串——但在 TypeScript 中,通过联合类型,这个操作就变得非常简单:

printLabel = (label: string|number) => string {
  console.log(`Please fill out ${label}`);
}

这是因为类型提示系统意图并不是要把动态语言变成静态语言,而是要让你用一种机器可以理解和执行的方式来记录你对类型的期望。这样,你可以在编译时或运行时发现并修复类型错误,提高代码的健壮性和可读性。

最后,需要指出的是,大部分类型提示系统都允许进行 渐进式类型化。在这种模式下,你可以逐个文件开启类型提示,而无需一次性转换整个代码库。你可以借助这个特性,逐步将你的代码迁移到你的类型系统中,而无需在一个项目中一次性完成所有转换。

651db68e211e8d733834bff7cccb5217.png

处理非结构化数据:字典的运用

在动态编程语言中,字典或哈希表的使用是一种常见的实践,它们被用于处理非结构化数据。当需要从函数返回数据或向函数传递选项时,字典通常是首选的数据结构:

def configure(options={})
  self.logger = options[:logger]
  self.host_name = options[:host_name]
end

或:

return { count: 5, average: 10}

由于字典可以用来存储任意的键值对,所以它们无法提供类型提示。使用字典来表达已知的数据会使得类型推导和错误查找变得困难。在第一个例子中,如果你不慎将 :loger 作为 :logger 的输入,错误将无法被抛出,而仅仅是把 logger 设为 null,这并不是你所期望的,因为这是一个调用代码中的错误,而不是数据中的错误。

对于支持接口或鸭子类型(动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。)的类型提示系统,如 TypeScript,你可以继续使用字典:

interface CountResult {
  count: number;
  average: number;
}
getCount : CountResult = () => { return { count: 5, average: 10} };

对于不支持该特性的编程语言,你可以选择使用结构体或数据类,这些类型简单且通常是不可变的,包含了一组字段:

ConfigurationOptions = Struct.new(:logger, :host_name)


def configure(options=ConfigurationOptions.new)
  self.logger = options.logger
  self.host_name = options.host_name
end

从索引表示法转为点表示法这一简单步骤引入了一种类型安全性级别。这样的转变可以增强代码的健壮性,即使在完全没有类型提示系统的情况下也可以实现。如果某个方法不存在,代码将会抛出一个错误,让你可以立即发现问题。

即使你没有使用类型提示系统,你也可以注释你的字段。许多 IDE 都能从这些注释中获取信息,进而实现自动补全。虽然这可能使定义变得更冗长,但是却更易于推理:

class ConfigurationOptions
  # @return [Logger]
  attr_accessor :logger
  # @return [String]
  attr_accessor :host_name

b066ca9c8a9f91ef75589af33466e117.png

明确地表达你的代码的意图和逻辑

静态语言强调明确性,有时候甚至给人过于严格的印象。你想知道一段代码在做什么?那就简单地用 ctrl+点击进入,然后沿着调用栈向下追溯。这里没有任何难以理解的“神秘”操作。

对于动态语言以及像 C 这样允许定义宏的语言,这种追踪过程就变得更加困难。动态语言经常无法通过“grep 测试” —— 我在阅读代码时,如果看到一个方法或特定的语法,我能否通过搜索整个代码库或者依赖来找到它的定义?如果找不到,那可能就意味着存在过多的元编程。

实际上,应该进行元编程或反射的只有框架本身。设计自己的内部框架是完全没有问题的,特别是它针对解决你的团队或公司特有的常见问题,那就是框架的最佳应用。但是,你不能假设他人理解你提供给他们的每一个细节影响,除非这些影响是明确地声明的。

如何判断自己的代码是否足够明确呢?当面对一个问题,你或许会有这样的想法,“我可以简洁地解决这个问题,或者我可以花费时间将所有的细节一一呈现出来,尽管这会花费我更多的时间。” 在这种情况下,你应该选择花费更多的时间来呈现细节。

让我们看个例子。在我们的 Rails 应用中,有许多与传单相关的工作类型。定义这些关联的一种方式需要遍历这些工作类型:

JOB_TYPES = [:process_image, :upload_image, :process_tagging]
JOB_TYPES.each do |type|
  has_many "#{type}_jobs"
end

这段代码能够正常工作,但如果我在代码库的某个地方看到 flyer.process_image_jobs,我应该如何知道它来自何处呢?

更直接的方式虽然工作量稍大,但是却更清晰:

has_many :process_image_jobs
has_many :upload_image_jobs
has_many :process_tagging_jobs

总的来说,你应该遵循最少惊讶原则。具体来说就是:

  • 不要对你无权修改的对象,尤其是基础类型,进行动态打补丁或更改行为。

  • 不要通过编程的方式创建方法;确保你可以在你的代码库。

  • 尽量不要使用隐式状态(例如在类的作用域之外使用 JavaScript 的 this 关键字)。明确指出正在操作的是什么对象。

  • 避免隐式地自动关联代码(例如,通过读取特定的文件结构,StimulusJS 就是这样做的,这是我对其它一些优秀的包的不满之处)。

当然,有些情况下我们可能需要一些“花哨”的操作,但是你需要明智地选择使用的场景!

cd5b3cd90eec427284babea60a9009c9.png

要经得住诱惑

动态语言给你很多自由,但也有很多风险,就像有句话说的,给你无限的空间让你自取灭亡。你应该享受自由,但也要尽可能地像静态语言那样思考,遵循一些规范和原则,避免一些隐患和错误。

对于如何更好地使用动态语言,你有何经验?欢迎在评论区交流经验。

推荐阅读:

▶微信:有零钱的微信号不会被系统注销;拼多多旗下 Temu 在美起诉 Shein;Rust 1.71.0 发布|极客头条

▶“仅 1 行代码,我们改了 6 天!”

▶我是如何使用ChatGPT和CoPilot作为编码助手的

你可能感兴趣的:(最小化动态编程语言的缺点)