DSL 迁移

2009 年 2 月 4 日

DSL 支持者需要警惕的一个危险是,首先设计 DSL,然后人们使用它。像任何其他软件设备一样,成功的 DSL 会不断发展。这意味着在 DSL 的早期版本中编写的脚本在使用更高版本运行时可能会失败。

像 DSL 的许多属性一样,好的和坏的,这实际上与库发生的情况非常相似。如果你从某人那里获取一个库,他们升级了库,你最终可能会陷入困境。本质上,DSL 并没有真正改变这一点。你的 DSL 定义本质上是一个 PublishedInterface,你必须以同样的方式处理其后果。

这个问题在外部 DSL 中可能更为突出。对内部 DSL 的许多更改可以通过重构工具来处理(对于那些有重构工具的语言)。但重构工具不会帮助外部 DSL。在实践中,这个问题不像看起来那么严重。内部 DSL 具有不受 DSL 实现者控制的脚本,不会被重构工具识别。因此,内部和外部之间的唯一区别在于同一代码库中的 DSL 脚本。

处理 DSL 演进的一种技术是提供工具,这些工具可以自动将 DSL 从一个版本迁移到另一个版本。这些工具可以在升级期间运行,或者在尝试对新版本运行旧版本脚本时自动运行。

处理迁移有两种主要方法。第一种是增量迁移策略。这与人们进行 演化数据库设计 时使用的概念基本相同。对于对 DSL 定义的每次更改,创建一个迁移程序,该程序可以自动将 DSL 脚本从旧版本迁移到新版本。

增量迁移的一个重要部分是尽可能保持更改的最小化。假设你正在从版本 1 升级到版本 2,并且想要对 DSL 定义进行十项更改。在这种情况下,不要只创建一个迁移脚本将版本 1 迁移到版本 2,而是至少创建 10 个脚本。一次更改一个 DSL 定义功能,并为每个更改编写一个迁移脚本。你可能会发现将它分解得更细,并用多个步骤(因此多个迁移)添加一个功能很有用。我描述的方式可能听起来比单个脚本工作量更大,但关键是,如果迁移很小,它们就更容易编写,并且很容易将多个迁移链接在一起。因此,编写十个脚本比编写一个脚本要快得多。

另一种方法是基于模型的迁移。如果你正在使用 语义模型(我几乎总是推荐这样做),你可以使用这种策略。使用这种方法,你为你的语言支持多个解析器,每个发布版本一个。(所以你只对版本 1 和 2 做这个,而不是对中间步骤做这个。)每个解析器填充语义模型。当你使用语义模型时,解析器的行为非常简单,因此拥有多个解析器并不麻烦。然后,你为正在使用的脚本版本运行相应的解析器。这处理了多个版本,但不会迁移脚本。要进行迁移,你需要从语义模型编写一个生成器,该生成器生成 DSL 脚本表示。这样,你可以运行版本 1 脚本的解析器,填充语义模型,然后从生成器发出版本 2 脚本。

基于模型方法的一个问题是,很容易丢失对语义不重要的东西,但脚本编写者想要保留的东西。注释是明显的例子。如果解析器中存在太多智能,这会加剧这种情况,但需要以这种方式迁移可能有助于鼓励解析器保持愚蠢——这是一件好事。

如果对 DSL 的更改足够大,你可能无法将版本 1 脚本转换为版本 2 语义模型。在这种情况下,你可能需要保留版本 1 模型(或中间模型),并赋予它发出版本 2 脚本的能力。

我对这两种选择没有强烈的偏好。

迁移脚本可以在需要时由脚本程序员自己运行,也可以由 DSL 系统自动运行。为了自动运行,让脚本记录它使用的 DSL 版本非常有用,这样解析器可以轻松地检测到它并触发相应的迁移。