模块设计

使程序结构化是编程的一个微妙的方面。 任何有价值的功能都可以用各种方式建模。

良好的程序设计是主观的 - 涉及到权衡和品味问题。 了解结构良好的设计的价值的最好方法,是阅读或处理大量程序,并注意哪些是有效的,哪些不是。 不要认为一个痛苦的混乱就是“它本来的方式”。 通过多加思考,你可以改善几乎所有事物的结构。

模块设计的一个方面是易用性。 如果你正在设计一些旨在由多人使用,或者甚至是你自己的东西,在三个月之内,当你记不住你所做的细节时,如果你的接口简单且可预测,这会有所帮助。

这可能意味着遵循现有的惯例。 ini包是一个很好的例子。 此模块模仿标准 JSON 对象,通过提供parsestringify(用于编写 INI 文件)函数,就像 JSON 一样,在字符串和普通对象之间进行转换。 所以接口很小且很熟悉,在你使用过一次后,你可能会记得如何使用它。

即使没有能模仿的标准函数或广泛使用的包,你也可以通过使用简单的数据结构,并执行单一的重点事项,来保持模块的可预测性。 例如,NPM 上的许多 INI 文件解析模块,提供了直接从硬盘读取文件并解析它的功能。 这使得在浏览器中不可能使用这些模块,因为我们没有文件系统的直接访问权,并且增加了复杂性,通过组合模块与某些文件读取功能,可以更好地解决它。

这指向了模块设计的另一个有用的方面 - 一些代码可以轻易与其他代码组合。比起执行带有副作用的复杂操作的更大的模块,计算值的核心模块适用于范围更广的程序。坚持从磁盘读取文件的 INI 文件读取器, 在文件内容来自其他来源的场景中是无用的。

与之相关,有状态的对象有时甚至是有用的,但是如果某件事可以用一个函数完成,就用一个函数。 NPM 上的几个 INI​​ 文件读取器提供了一种接口风格,需要你先创建一个对象,然后将该文件加载到对象中,最后使用特定方法来获取结果。这种类型的东西在面向对象的传统中很常见,而且很糟糕。你不能调用单个函数来完成,你必须执行仪式,在各种状态中移动对象。而且由于数据现在封装在一个特定的对象类型中,与它交互的所有代码都必须知道该类型,从而产生不必要的相互依赖关系。

通常,定义新的数据结构是不可避免的 - 只有少数非常基本的数据结构由语言标准提供,并且许多类型的数据一定比数组或映射更复杂。 但是当数组足够时,使用数组。

一个稍微复杂的数据结构的示例是第 7 章的图。JavaScript 中没有一种明显的表示图的方式。 在那一章中,我们使用了一个对象,其属性保存了字符串数组 - 可以从某个节点到达的其他节点。

NPM 上有几种不同的寻路包,但他们都没有使用这种图的格式。 它们通常允许图的边带有权重,它是与其相关的成本或距离,这在我们的表示中是不可能的。

例如,存在dijkstrajs包。 一种著名的寻路方法,与我们的findRoute函数非常相似,它被称为迪科斯特拉(Dijkstra)算法,以首先编写它的艾兹格尔·迪科斯特拉(Edsger Dijkstra)命名。 js后缀通常会添加到包名称中,以表明它们用 JavaScript 编写。 这个dijkstrajs包使用类似于我们的图的格式,但是它不使用数组,而是使用对象,它的属性值是数字 - 边的权重。

所以如果我们想要使用这个包,我们必须确保我们的图以它期望的格式存储。 所有边的权重都相同,因为我们的简化模型将每条道路视为具有相同的成本(一个回合)。

  1. const {find_path} = require("dijkstrajs");
  2. let graph = {};
  3. for (let node of Object.keys(roadGraph)) {
  4. let edges = graph[node] = {};
  5. for (let dest of roadGraph[node]) {
  6. edges[dest] = 1;
  7. }
  8. }
  9. console.log(find_path(graph, "Post Office", "Cabin"));
  10. // → ["Post Office", "Alice's House", "Cabin"]

这可能是组合的障碍 - 当各种包使用不同的数据结构来描述类似的事情时,将它们组合起来很困难。 因此,如果你想要设计可组合性,请查找其他人使用的数据结构,并在可能的情况下遵循他们的示例。