使用 JavaScript 设计模型

2025-06-08

使用 JavaScript 设计模型

我的祖母是一位“brodeuse”(法语,意为刺绣师)。她为我家制作精美的传统服饰,我们珍藏至今。最近,在打扫她家时,我们发现了一件宝贝——她制作衣服时使用的图样。她过去常常把图样画在大大的纸上。通过观察图样,我们就能清楚地知道她想要制作什么样的衣服。

我实在不想拿我们的工作来类比。在开发过程中,我们太多次地回避了设计这个基本步骤。我们在设计应用程序模型之前就开始编码,只是因为我们认为代码就是模型。但事实并非如此。代码只是构成应用程序的骨架。通过阅读代码,我们只能猜测应用程序的创建使用了什么模型和模式。此外,由于我们是人类,由于开发人员的思维方式各不相同,我们还需要了解开发人员在编码时的思维空间组织方式。而这并非易事。

关注点分离

这就是为什么无论你使用什么框架、编写什么代码,最重要的是你为创建应用程序而定义的模型。而且这个模型不一定非要在代码中而必须在其他地方,以人类可读的格式规范化。但是什么格式呢?我们可以使用UML,它现在很常见,并且有许多设计工具使用它。但问题是它不是一种格式,而是一种语言。它非常复杂,当你需要理解图表时,它不太人性化。我知道 UML 图可以保存为XMI格式,但它是给机器看的,而不是给人看的。

创建模型的模式

让我们通过一个例子来找到我们需要的格式。假设我们要创建一个绝地武士的模型。绝地武士由其名字姓氏定义,她/他有母亲父亲,并且可以生育孩子。假设我们将所有与此模型相关的信息放入一个 JSON 文件中,我们可以得到如下内容:

{
  "_name": "Jedi",
  "firstName": "property",
  "lastName": "property",
  "mother": "link",
  "father": "link",
  "children": "collection"
}
Enter fullscreen mode Exit fullscreen mode

阅读这个 JSON 时,我们很容易理解:

  • 绝地武士有两个属性firstNamelastName
  • 绝地武士母亲父亲息息相关
  • 绝地武士一群孩子

这个 JSON 实际上是我们模型的模式。

生成模型并扩展它

能够定义这样的模型真是太棒了,对吧?现在我们有了这个模式,让我们更进一步。如果我们能从模型生成 UML 类图,得到如下结果会怎样呢?

模型

为了实现这一点,我们缺少一些基本信息,例如属性的类型。如何处理我们的文件呢?只需从架构中生成一个具有默认值的完整模型,然后对其进行编辑即可

{
  "_name": "Jedi",
  "firstName": {
    "type": "any",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "lastName": {
    "type": "any",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "mother": {
    "type": "Component",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "father": {
    "type": "Component",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "children": {
    "type": ["Component"],
    "readOnly": false,
    "mandatory": false,
    "default": []
  }
}
Enter fullscreen mode Exit fullscreen mode

这里我们把所有定义在schema中的信息都以人类可读的格式进行了编辑。例如,对于firstName ,我们可以将其类型(默认值为any,即任意类型)设置为string

从这个 JSON 可以轻松生成 UML 类图。我们有一个完整的模型来构建应用程序。

在运行时使用模型

那么代码呢?在模型驱动架构方法中,我们根据模型(通常是 UML 图)生成代码。这种方法很棒,但并不完美,因为我们定义的模型与代码不同步的风险更高。那么如何避免这种情况呢?只需在运行时使用应用程序的模型定义即可。应用程序必须读取我们定义的模式和模型,才能创建模型的类、方法和组件

是的,看起来很酷,但如果这些方法是在运行时生成的,我该如何实现呢?要做到这一点,你需要系统地思考方法只是系统对事件做出反应时执行的操作。 

在我们的例子中,假设我们需要在Jedi中添加一个方法来获取她/他的全名。因此,我们将修改架构使其如下所示:

{
  "_name": "Jedi",
  "firstName": "property",
  "lastName": "property",
  "mother": "link",
  "father": "link",
  "children": "collection",
  "fullName": "method"
}
Enter fullscreen mode Exit fullscreen mode

我们在schema添加了fullName 方法。因此,在生成的模型中,我们将拥有:

{
  "_name": "Jedi",
  "firstName": {
    "type": "any",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "lastName": {
    "type": "any",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "mother": {
    "type": "Component",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "father": {
    "type": "Component",
    "readOnly": false,
    "mandatory": false,
    "default": ""
  },
  "children": {
    "type": ["Component"],
    "readOnly": false,
    "mandatory": false,
    "default": []
  },
  "fullName": {
    "params": [
      {
        "name": "param",
        "type": "any",
        "mandatory": false,
        "default": null
      }
    ],
    "result": "any"
  }
}
Enter fullscreen mode Exit fullscreen mode

默认情况下,fullName接受一个可选参数,并可以返回任意类型的值。现在假设我们想要实现这个方法。该怎么做呢?只需在发送 fullName 事件时添加一个行为即可:

// require the Jedi class
const Jedi = runtime.require('Jedi');
// add a behavior
Jedi.on('fullName', () => this.firstName() + ' ' + this.lastName());
Enter fullscreen mode Exit fullscreen mode

那么这个事件什么时候发送呢?就是在代码中调用fullName方法的时候。实际上,这个方法已经从模型中生成了,当我们调用它时,它只会发送一个同步的fullName事件。然后,与该事件相关的现有行为就会被执行。

现在,如果我们改变方法的模型,它将永远不会覆盖其行为。

结论

设计模型时要遵循以下不同步骤:

过程

  • 以人类可读的格式在高层次上定义您的模型
  • 将从该模式生成一个模型。
  • 编辑生成的模型以指定模型的类型、默认值等。
  • 使用此模型在运行时创建模型的类、方法和组件
  • 使用事件驱动的方法构建您的应用程序,以向方法添加行为
  • 使用您的模型生成 UML 类图

如果您想了解这种方法的具体用途,可以看看我的两个项目:

深入运用此方法论。为此,我创建了MSON格式(即Metamodel JavaScript Object Notation),以便以人类可读的格式定义模型。但这并非此方法论的唯一实现,您可以找到符合您需求的实现。


致谢:封面图片由Christian Kaindl提供。

鏂囩珷鏉ユ簮锛�https://dev.to/ecarriou/designing-models-in-javascript-53je
PREV
VSCode 中的 TODO 列表
NEXT
通过让正确的事情变得最简单来养成习惯