这几年一直在做各种领域定义语言的设计,包括unflow、guarding、datum、forming等等。刚开始接触这个领域的时候,我是从《领域特定语言》学习的,《编程语言实现模式》等龙树。逐渐掌握了领域特定语言设计的一些技巧,可以很快(相比过去)设计领域特定语言。于是,我就在想,我应该总结一下相关的套路。这样,以后也可以验证当前的思路是否正确:定义呈现方式。精炼领域特定名词。设计联想和语法。实现语法分析。发展语言的设计。领域特定语言领域特定语言(英语:domain-specificlanguage,DSL)是指专注于某一应用领域的计算机语言。本文所写的一切都是外部DSL,即一种“与应用系统使用的主要语言不同”的语言。创建外部DSL的过程类似于创建可以编译或解释的通用编程语言。通用编程语言的源代码与外部DSL的源代码之间的主要区别在于,编译后的DSL通常不会直接生成可执行程序(但它会生成)。在大多数情况下,外部DSL可以转化为与核心应用程序运行环境兼容的资源,或者转化为用于构建核心应用程序的通用编程语言。——VaughnVernon简单场景下的领域特定语言只是将特定的源代码转换成特定的数据结构。例如,JSON是一种DSL。在Java语言中,需要转换成对应的数据类。复杂场景下的领域特定语言可以直接编译成可执行程序。外部DSL的麻烦是:语法设计语法解析IDE支持当然,它的优势也很明显:它可以让不懂编程的业务专家(领域专家)快速写出核心逻辑。领域逻辑与特定的编程语言和平台无关。想了解更多,推荐阅读《领域特定语言》一书。定义展现方式的领域特定语言是业务展现在需求上的一种简化。根据不同的呈现方式,解析源码得到我们需要的数据结构。呈现方式如下是一种常见的领域特定语言使用模式[wiki_dsl]:单机工具,如Makefile,在编译时或实时转换为宿主语言嵌入式领域特定语言...见维基百科,我就不翻译了。[wikidsl]:https://en.wikipedia.org/wiki/Domain-specificlanguage定义数据结构从通用语言编译过程来看:词法分析器对输入的字符流进行分析,得到符号流。语法分析,分析符号流,得到语法树语义分析,分析语法树,得到新的语法树中间代码生成器,分析语法树,得到中间表示...步骤1~4,对于通用语言和domainspecificlanguage据说非常相似。唯一的区别是这个中间表示。对于特定领域的语言,我们场景的原因往往是我们需要的数据结构。当然,从某种意义上说,AST(AbstractSyntaxTree)也是一种数据结构,只不过是一种中间数据结构。因此,有时候在设计的时候偷懒,直接输出中间表示。抽取特定领域名词的过程在实现上与DDD(领域驱动设计)中抽取问题域以获取领域知识非常相似。在同一过程中,我们可以通过与领域专家的协作获得更好的领域特定语言。从用例出发,用例,或译为用例,用例,是软件工程或系统工程中对系统如何响应外部请求的描述,是一种通过用户使用场景获取需求的技术。在进行领域驱动设计协作时,我们需要与领域专家一起了解用户在这个过程中进行的一系列操作,从而提炼出我们需要的统一语言。而用例可以描述实现目标所需的步骤,包括用户与系统之间的交互。创建特定领域的语言时,过程与我们类似:与领域专家合作并从用例中提炼。它也可以直接从现有代码中提取。从现有用例开始对于现有系统,可以通过以下方式获得用例:与领域专家交流。与领域专家聊天是我们获取用例的最佳方式。记录用例以获取关键信息。从现有代码中提取。提取ArchUnit中的架构规划设计是:classes().that().resideInAPackage("..foo..").should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..",")。.f对应,我们在Guarding中的设计是:class(resideIn"..foo..")dependentpackage(resideIn["..source.one..","..foo.."]在Guarding中是为主流的编程语言,所以在语法上会尽量独立于编程语言。在获取用例作为输入条件后,我们需要提取一些关键信息,比如关键字,值,属性等。下面是我在设计GuardingDSL时从ArchUnit中提取的一小部分关键信息:主要是为了实现领域中的用例:用一个DSL来描述一个用途不考虑语法实现的情况,实现大部分用例的DSL草案版本,以对齐不同用例在DSL中的差异,考虑一些非常规用例,增加额外的属性名词关系和逻辑设计领域特定语言,旨在一个特定的域。在特定的领域中,使用特定的词汇来描述相关的关系。这种关系,是我们语法设计的关键。比如在Java语言中,用:implement,extends来表示两个类之间的关系。为了表示包之间的关系,会有:dependent、resideIn等关系。实现用例实现用例并不是一个复杂的过程,而是要符合人的思维习惯,尽可能简化设计。但是,我觉得我们应该留下一些证据来告诉未来的自己:我们当时为什么被考虑。在设计DSL时,我经常创建一个示例文件来记录我在这个过程中对不同元素的想法。例如,在设计GuardingDSL时,我使用了一个0.0.1.sample文本文件来描述早期版本的语法示例:#regularexpressionpackage(match("^/app"))endsWith"Connection";package("..home..")::nameshouldnotcontains(matching(""));#简化比较类::name.lenshould<20;让自己通过一些评论来优化设计。语法分析部分的实现过程和我们学习编译原理时基本相同。然而,在编写领域特定语言时,我们通常使用解析器生成器而不是手写解析器。详细设计在设计特定领域的语言时,设计语法上没有通用语言那么多的约束。所以自由设计的范围更大,有些内容不一定需要像编程语言那样麻烦。如:定界符、缩进处理、句法块的起止……PS:使用类编程语言的写法对于写DSL的非程序员来说可能会成为一种迷惑。parsergenerator经典的Lex&Yacc是你可以考虑的范围,不同语言也有一些类似的实现。对我来说,这里有一些我经常使用的解析器生成器。蚂蚁支持主流语言Peg.js。JavaScript害虫。锈Larrpop。我还是比较习惯用AntlrforRust,它支持很多语言。我的同事和开源社区的朋友在以下项目中使用了Antlr:Coca=Golang+AntlrUnflow=Rust+AntlrLemonj=JavaScript/TypeScript+AntlrChapi=Java/Kotlin+Antlr从使用他们到他们之间的差距是不大,但都需要学习成本。不断发展的语言设计最后,让我们谈谈一些有趣的事情。虽然是进化,但暂时和设计无关。测试驱动开发我发现TDD非常适合编程语言开发和设计。需求未知,易变,需要覆盖足够多的场景。从实践层面来看,主要有两种类型:面向语法的测试。即只有语法编译可以通过,但不报错。面向功能的测试。也就是验证某部分的语法是否正确。面向用例的测试。即验证是否符合使用场景。本节关于自动语言迁移的原始标题是“向后兼容性”。但是,我一直觉得向后兼容是个坏主意。于是,我想了想,想把自己在其他领域的经验迁移过来,于是内容就变成了自动语言迁移。在版本迁移方面,我觉得Angular语言的版本自动化迁移值得借鉴。当然采用这种设计的成本很高,我们需要有专门的团队,用工具自动分析旧系统,用工具自动修改旧代码。其他文章相关DSL链接(欢迎加入Inherd一起写DSL):Unflow:https://github.com/inherd/unflowGuarding:https://github.com/inherd/guardingForming:https://github.com/inherd/forming本文转载自微信公众号「phodal」,可通过以下二维码关注。转载本文请联系phodal公众号。
