Blog

Blog

PHODAL

如何为代码建模?

去年年底,在公司大佬的带领下,我们结合架构守护的需要,对代码进行了简单的建模。在过去的几个月里,我一直工作在相关的事项上,不断地优化、改进相关的模型:

  • 重构 Coca 的模型,以支持 Java 以外的语言
  • 基于 Kotlin MultiPlatform 技术重写模型,以在未来提供多平台、语言的支持

所以,大致上有了现在称之为 3.0 版本的代码模型。花了这么大的力气干活,还是应该分享一下相应的经验。这样一来,不 996 的你,也许能做出更好的轮子。[狗头][狗头]

引子 1:文本即代码,代码即测试数据

PS:在那一篇《如何同时学会两门编程语言?》中,我大抵提到了这一小节的内容,所以它对你来说可能有些重复。

首先,让我们来看段代码。如下是一段 Scala 语言编写的 hello, world :

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("hello, world!")
  }
}

哦,不对, 它真的是一段代码吗?哦,不对,在这里它的用途不是给机器执行的,它的用途只是一段测试用例:

@Test
internal fun shouldIdentObjectName() {
    val container = ScalaAnalyser().analysis(helloworld, "hello.scala")
    assertEquals(container.DataStructures.size, 1)
    assertEquals(container.DataStructures[0].NodeName, "HelloWorld")
    assertEquals(container.DataStructures[0].Type, DataStructType.OBJECT)
}

反过来,这些测试文本也需要是真正可以工作地代码。更多地测试示例可以见:https://github.com/phodal/chapi

引子 2:代码即语法,语法即代码

将代码转换为特别的模型,我们还需要做的一件事情是:识别代码。代码本身是依照特别规则编写的字符串。这些特定的代码规则便是语法。也因此,如果我们要将不同的编程语言的源码转为模型,就需要不同语言的语法。

如下是一个 TypeScript 的 function 语法规则的部分代码示例:

functionExpressionDeclaration
    : Function Identifier? '(' formalParameterList? ')' typeAnnotation? '{' functionBody '}'
    ;

它可以匹配上下面的代码:

function sum(x: number, y: number) : void {
    return x + y;
}

对应的便可以映射上代码:

  • Function -> 'function'
  • Identifier -> 'sum'
  • formalParameterList -> 'x: number, y: number'
  • typeAnnotation -> ': void'
  • functionBody -> 'return x + y'

这样一来,我们就能迈向下一步了,抽取代码模型。

引子 3: 代码即模型

在通信和信息处理领域,代码(code)是指一套转换信息的规则系统,例如将一个字母、單詞、声音、图像或手势转换为另一种形式或表达,有时还会缩短或加密以便通过某种信道或存储媒体通信。

所以,我们可以先简单地把代码视为:行为 + 数据结构,它们统一称为模型。而模型又分为两种数据结构的模型行为的模型的模型。举个例子,在 Golang 中,我们使用 struct 作为结构体,来存储同一类型的数据:

type Books struct {
   title string
   author string
   subject string
   book_id int
}

如果只是这样的代码,我们可以轻松地把它转换为数据结构。

在计算机科学中,数据结构是计算机中存储、组织数据的方式。

然后,还有行为呢?行为事实上,就是各种表达式,而表达式,归根到底还是各种各样的模式,因为我们需要存储这些表达式。

代码描述代码,模型描述模型

终于,我们回到了正题:如何用代码描述代码。事实上,我们已经讲完了这个故事的大纲,剩下的就只是一些连线了。

好激动,我们终于要开始造轮子了,那么我们要怎么开始呢?

0. 寻找语法解析器及现成语法

市面上已经有一系列现成的词法解析器、语法解析器:

  • JavaCC
  • Lex 和 Yacc
  • Flex 和 Bison
  • Jison (for JavaScript)
  • Parsec
  • Antlr(for All)

最后,我选择了用 Antlr,因为公司的大佬们告诉我用 Antlr:先用 Antlr 解析它们,再写个 Antlr-like 来解析它们,再写个语言来写解析器。这个楼,有点歪。

大家选择 Antlr 的主要原因,Antlr 官方维护着社区贡献的各种语言的 Antlr 编写的语法:https://github.com/antlr/grammars-v4/

1. 设计代码模型

我们已经有足够的知识,来将一段代码转为数据模型,并设计一个测试体系来保障代码的健壮性(测试 + TDD)。接下来,便是要以迭代式的方式来设计一系列模型作为容器,容纳一段一段的代码:

  • 包体系与结构
  • 结构体/函数体
  • 继承体系
  • 函数结构
  • ……

这是一个复杂的过程,而且我保证现在的模型不是完整的,所以等我真正写完 Chapi 之后,我会重新起一篇文章来解释这个模型。在那之前,请阅读代码:https://github.com/phodal/chapi

因为我也是从一个简单的模型开始,重构了三次之后,才有了一个勉强可以工作地模型。

2. 将代码数据放到容器中

在我们有了模型之后,我们便可以编写模型的代码,作为容器来放置内容。

@Serializable
open class CodeCall(
    var Package: String = "",
    var Type: String = "",
    var NodeName: String = "",
    var FunctionName: String = "",
    var Parameters: Array<CodeProperty> = arrayOf<CodeProperty>(),
    var Position: CodePosition = CodePosition()
) {
  ...
}

这样一来,我们可以 “完整” 的将代码转为模型,还有对应的序列化的 JSON 结果。

[{"NodeName":"Outer","Type":"CLASS","Package":"","FilePath":"hello.scala","Fields":[],"MultipleExtend":[],"Implements":[],"Extend":"","Functions":[],"InnerStructures":[{"NodeName":"Inner","Type":"OBJECT","Package":"","FilePath":"hello.scala","Fields":[],"MultipleExtend":[],"Implements":[],"Extend":"","Functions":[],"InnerStructures":[],"Annotations":[],"FunctionCalls":[],"Parameters":[],"InOutProperties":[],"Imports":[],"Extension":{}}],"Annotations":[],"FunctionCalls":[],"Parameters":[{"Modifiers":[],"DefaultValue":"","TypeValue":"i","TypeType":"Int","ReturnTypes":[],"Parameters":[]}],"InOutProperties":[],"Imports":[],"Extension":{}}]

然后存储到数据库中。

3. 打包容器成项目

进一步地,如果我们想以项目的维度来构建出完整的模型,我们还需要代码包的相关信息。考虑到和代码相关度不高,有兴趣的同学可以参考 Coca 中的源码: https://github.com/phodal/coca 。

4. 识别不同语言中的细节

细节是魔鬼。

尽管我们针对于类、函数已经有解了,但是仍然还有不同语言的核心部分还需要有解决:

  • 高阶函数(Higher-order functions)。
  • 嵌套函数(Nested functions)。
    • 命名(Named)
    • 匿名
  • 嵌套数据结构/类。

基于此,我们需要进一步完善模型。

5. 应对奇技淫巧

如我们在 Chapi 大本营里讨论的,还有各种奇怪的代码,如 C 语言的:

for(int i=0, j= 0; i<20&&j<30; i++) {
    j++;
}

我想不出来他们为什么要这么写,但是你还是要编译成功。

6. 重复步骤 4~6

还有,技巧 1: All in One Test

你需要一个包含所有语法的文件,以避免 NullException。

以及,技巧 2:回归测试

在真实项目运行的时候,记得编写测试,以确保下次是正常的。

下一步,表达式转为模型

如何将表达式准确转换为类型?


或许您还需要下面的文章:

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806

新书《全栈应用开发:精益实践》

这不是一本深入前端、后台、运维、设计、分析等各个领域的书籍。本书以实践的方式,将这一系列的领域及理论知识结合到一起,来帮助读者构建全栈Web 开发的知识体系,并辅以精益及敏捷的思想,来一步步开发Web 应用:从创建一个UI 原型到编写出静态的前端页面;从静态的前端页面到带后台的应用,并部署应用;从Web 后台开发API 到开发移动Web 应用。在这个过程中,我们还将介绍一些相辅相成的步骤:使用构建系统来加速Web 应用的开发;为应用添加数据分析工具来改进产品;使用分析工具来改善应用的性能;通过自动化部署来加快上线流程;从而帮助读者开发出一个真正可用的全栈 Web 应用。同时,我们也将帮助读者把这些步骤应用到现有的系统上,改进现有系统的开发流程。

comment

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Developer, Consultant, Writer, Designer

ThoughtWorks 高级咨询师

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 与我沟通

标签