一杆到底:DSL 领域特定语言

您所在的位置:网站首页 编程专用词汇有哪些 一杆到底:DSL 领域特定语言

一杆到底:DSL 领域特定语言

2024-07-15 23:20| 来源: 网络整理| 查看: 265

一、DSL了解 1、DSL介绍

DSL(Domain Specific Language)是针对某一领域,具有受限表达性的一种计算机程序设计语言。 常用于聚焦指定的领域或问题,这就要求 DSL 具备强大的表现力,同时在使用起来要简单。说到DSL,大家也会自然的想到通用语言(如Java、C等)。

为什么没有一种语言同时 兼具『简洁』和『业务表达』能力呢?

从信息论本质上来讨论这个问题,每个语言的程序都可以抽象为一个字符串,每个字符串由有限数量的合法字符组成,它在运行时会实现某个功能,因而可以看作是一种需求的信源编码。每种需求可以映射到一个或多个正确的程序,但一个程序肯定只对应到一种需求,因而程序包含的信息熵不低于需求的信息熵。而程序中不仅仅需要描述需求的信息,还需要包含 可读性、辩识度,如果是静态语言还需要 静态检查等额外信息。 这里也可以看出来,为什么DSL是特定领域的语言了。

2、DSL分类

最常见的分类方法是按照DSL的实现途径来分类。马丁·福勒曾将DSL分为内部和外部两大类,他的分类法得到了绝大多数业界人士的认可和沿袭。内部与外部之分取决于DSL是否将一种现存语言作为宿主语言,在其上构建自身的实现。

2.1、内部DSL

也称内嵌式DSL。因为它们的实现嵌入到宿主语言中,与之合为一体。内部DSL将一种现有编程语言作为宿主语言,基于其设施建立专门面向特定领域的各种语义。例如:Kotlin DSL、Groovy DSL等;

2.2、外部DSL

也称独立DSL。因为它们是从零开始建立起来的独立语言,而不基于任何现有宿主语言的设施建立。外部DSL是从零开发的DSL,在词法分析、解析技术、解释、编译、代码生成等方面拥有独立的设施。开发外部DSL近似于从零开始实现一种拥有独特语法和语义的全新语言。构建工具make 、语法分析器生成工具YACC、词法分析工具LEX等都是常见的外部DSL。例如:正则表达式、XML、SQL、JSON、 Markdown等;

3、DSL示例

3.1、内部DSL

HTML: 通过自然语言编写

在Groovy中,通过DSL可以用易读的写法生成XML

import groovy.xml.MarkupBuilder def s = new StringWriter() def xml = new MarkupBuilder(s) xml.html{     head{         title("Hello")         script(ahref:'https://xxxx.com/vue.js')     }     body{         p("Excited")     } } println s.toString()

最后将生成

                       

Excited

  

这里相对于Java这样的动态语言,最为不同的就是xml.html这个并不存在的方法居然可以通过编译并运行,它内部重写了invokeMethod方法,并进行闭包遍历,少写了许多POJO对象,效率更高。

3.2、外部DSL

以plantUML为例,外部DSL不受限于宿主语言的语法,对用户很友好,尤其是对于不懂宿主语言语法的用户。但外部DSL的自定义语法需要有配套的语法分析器。常见的语法分析器有:YACC、ANTLR等。

4、DSL & DDD(领域驱动)

DDD和DSL的融合有三点:

面向领域; 模型的组装方式; 分层架构演进;

DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。

它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。外部 DSL 就是对领域模型的一种组装方式。

5、DSL不是银弹

前开篇也提到了,在信息量不变的情况下,代码行数越短,它的“潜规则”信息量就越多,那么如何排查?如何定位?如何扩展?成为一个好的DSL需要考量的点。好的DSL难点在于:

DSL只是一种声明式的编程语言,无法承载大量业务。 DSL语句与编译生成的“字节码”的过程是黑盒的,不但对内部工作不明朗,如果报错的话,不但堆栈行数无法与源码对应上,而且无法“断点”或者“日志”。 DSL对设计者要求高,需要会一个领域有通透的理解,设计时要克制『增加各种特性』,DSL还要文档齐全,支撑充分,甚至要开源以帮助使用者定位。

二、有哪些工具

上节中提到,DSL分为内部和外部。由于外部DSL需要自己编写分析器,所以笔者使用 内部DSL实现。从之前收集的大量资料中,调研到 有两种比如轻量实现DSL的方式。

第一种:使用Groovy语言的元编程特性,天然支持DSL的下定义,而且兼容Java调用,生成的class更容易被JVM优化,执行性能上不会有太多损失。

第二种:使用Jetbrains MPS,开发基于java base的内部DSL。支持快速修复、智能提示、语法检查等。

https://www.jetbrains.com/zh-cn/mps/

下面我们将以:第一种 Groovy为基础语言,开发 内部DSL。

三、Groovy实战DSL 1、 实现原理 (1)闭包

官方定义是“Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量”

简而言之,他说一个匿名的代码块,可以接受参数,有返回值。在DSL中,一个DSL脚本就是一个闭包。

比如:

//执行一句话   { printf 'Hello World' }                                   //闭包有默认参数it,且不用申明       { println it }                    //闭包有默认参数it,申明了也无所谓                 { it -> println it }            // name是自定义的参数名   { name -> println name }                  //多个参数的闭包 { String x, int y ->                                     println "hey ${x} the value is ${y}"     }

每定义的闭包是一个Closure对象,我们可以把一个闭包赋值给一个变量,然后调用变量执行

//闭包赋值 def closure = {     printf("hello") } //调用 closure()

(2)括号语法

当调用的方法需要参数时,Groovy 不要求使用括号,若有多个参数,那么参数之间依然使用逗号分隔;如果不需要参数,那么方法的调用必须显示的使用括号。

def add(number) { 1 + number } //DSL调用 def res = add 1 println res

也支持级联调用方式,举例来说,a b c d 实际上就等同于 a(b).c(d)

//定义 total = 0 def a(number) {     total += number     return this } def b(number) {     total *= number     return this } //dsl a 2 b 3 println total

(3)无参方法调用

我们结合 Groovy 中对属性的访问就是对 getXXX 的访问,将无参数的方法名改成 getXXX 的形式,即可实现“调用无参数的方法不需要括号”的语法!比如:

def getTotal() { println "Total" } //DSL调用 total

(4)MOP

MOP:元对象协议。由 Groovy 语言中的一种协议。该协议的出现为元编程提供了优雅的解决方案。而 MOP 机制的核心就是 MetaClass。

有点类似于 Java 中的反射,但是在使用上却比 Java 中的反射简单的多。

常用的方法有:

invokeMethod() setProperty() hasProperty() methodMissing()

以下是一个methodMissing的例子:

detailInfo = [:] def methodMissing(String name, args) {     detailInfo[name] = args } def introduce(closure) {     closure.delegate = this     closure()     detailInfo.each {         key, value ->             println "My $key is $value"     } } introduce {     name "zx"     age 18 }

(5)定义和脚本分离

@BaseScript 需要在注释在自定义的脚本类型变量上,来指定当前脚本属于哪个Delegate,从而执行相应的脚本命令,也使IDE有自动提示的功能:

脚本定义

abstract class DslDelegate extends Script { def setName(String name){         println name     } }

脚本:

import dsl.groovy.SetNameDelegate import groovy.transform.BaseScript @BaseScript DslDelegate _ setName("name")

(6)闭包委托

使用以上介绍的方法,只能在脚本里执行单个命令,如果想在脚本里执行复杂的嵌套关系,比如Gradle中的dependencies,就需要@DelegatesTo支持了,@DelegatesTo执行了脚本里定义的闭包用那个类来解析。

上面提到一个DSL脚本就是一个闭包,这里的DelegatesTo其实定义的是闭包里面的二级闭包的格式,当然如果你乐意,可以无限嵌套定义。

//定义二级闭包格式 class Conf{     String name     int age     Conf name(String name) {         this.name = name         return this     }     Conf age(int age) {         this.age = age         return this     } } //定义一级闭包格式,即脚本的格式 String user(@DelegatesTo(Conf.class) Closure closure) {     Conf conf = new Conf()     DefaultGroovyMethods.with(conf, closure)     println "my name is ${conf.name} my age is ${conf.age}" } //dsl脚本 user{     name "tom"     age 12 }

(7)Java加载并执行脚本

脚本可以在IDE里直接执行,大多数情况下DSL脚本都是以文本的形式存在数据库或配置中,这时候就需要先加载脚本再执行,加载脚本可以通过以下方式:

 CompilerConfiguration compilerConfiguration = new CompilerConfiguration();  compilerConfiguration.setScriptBaseClass(DslDelegate.class.getName());  GroovyShell shell = new GroovyShell(GroovyScriptRunner.class.getClassLoader());  Script script = shell.parse(file);

给脚本传参数,并得到返回结果:

Binding binding = new Binding(); binding.setProperty("key", anyValue); Object res = InvokerHelper.createScript(script.getClass(), binding).run()

2、Groovy DSL示例

(1)需求

假设我们要做一个备忘录生成器。备忘录有to、from、body 三个字段,它也可以包含如 Summary、Important等动态字段,最后它可以以 xml、html、text三种格式输出。

Groovy中DLS实现后的效果,如下:

MemoDsl.make {     to 'Nirav Assar'     from 'Barack Obama'     body 'How are things? We are doing well. Take care'     idea 'The economy is key'     request 'Please vote for me'     xml }

输出结果如下(DSL的最后一行 决定输出格式,可以xml、html、text):

     Nirav Assar      Barack Obama       How are things? We are doing well. Take care      The economy is key      Please vote for me  

(2)实现

定义接收类

MemoDsl类中make静态方法,会创建一个MemoDsl实例,并委托给闭包。后续to、from方法,将调用到MemoDsl实例上,在调用to()方法后,文本将保存在实例中,以便稍后使用。

class MemoDsl {       String toText     String fromText     String body     def sections = []       // mark方法需要接受一个闭包,并委托closure方法到memoDsl,所以DSL方法才能生效     def static make(closure) {         MemoDsl memoDsl = new MemoDsl()         // 任务调用到闭包的方法,都将委托给memoDsl实例         closure.delegate = memoDsl         closure()     }      // 将参数保存到变量中,以便稍后使用     def to(String toText) {         this.toText = toText     }       def from(String fromText) {         this.fromText = fromText     }       def body(String bodyText) {         this.body = bodyText     } }

处理动态属性

当闭包包含了MemoDsl类不存在的方法时,groovy会将方法标识为缺失方法。它会通过groovy的元对象协议,调用到MemoDsl的methodMissing接口上。这也是我们能正确处理idea、request字段的原因。

MemoDsl.make {     to 'Nirav Assar'     from 'Barack Obama'     body 'How are things? We are doing well. Take care'     idea 'The economy is key'     request 'Please vote for me'     xml } 

处理缺失属性的方法如下:

// 当遇到缺失属性时,groovy通过元对象协议,调用methodMissing方法 def methodMissing(String methodName, args) {     def section = new Section(title: methodName, body: args[0])     sections 


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3