DSL 概念
DSL定义
领域专用语言(DSL)是一种计算机语言专用于一个特定的应用领域。这与通用语言(GPL)相反,后者广泛适用于各个领域。DSL种类繁多,从用于公共领域的广泛使用的语言(例如网页的HTML)到只有一种或几种软件使用的语言(例如MUSH软代码)。DSL可以根据语言的种类进一步细分,包括特定于域的标记语言,特定于域的建模语言(规范语言)和领域特定的编程语言。专用计算机语言在计算机时代一直存在,但是由于特定领域建模的兴起,术语“特定领域语言”变得越来越流行。简单的DSL,特别是单个应用程序使用的DSL,有时被非正式地称为迷你语言。
A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains. There are a wide variety of DSLs, ranging from widely used languages for common domains, such as HTML for web pages, down to languages used by only one or a few pieces of software, such as MUSH soft code. DSLs can be further subdivided by the kind of language, and include domain-specific markup languages, domain-specific modeling languages (more generally, specification languages), and domain-specific programming languages. Special-purpose computer languages have always existed in the computer age, but the term “domain-specific language” has become more popular due to the rise of domain-specific modeling. Simpler DSLs, particularly ones used by a single application, are sometimes informally called mini-languages.
更简短的定义:
- DSL(Domain-Specific Language):领域专用语言 或 领域特定语言
- DSL 是针对某一特定领域,具有受限表达性的一种计算机程序设计语言
- DSL 并不会一⻔新鲜的技术:SQL,HTML,CSS,正则表达式
- DSL 是最常⻅的声明式编程形式
DSL与通用编程语言的区别:
领域专用语言这个名字其实已经给出了答案。你应该牢记DSL最重要的两个特征:
- 一种DSL专门针对一个特定的问题领域;
- DSL含有建模所需的语法和语义,在与问题域相同的抽象层次 对概念建模。
DSL应用
特定于领域的语言是专门为解决特定领域中的问题而创建的,并非旨在解决其外部的问题(尽管在技术上可能是可行的)。相反,创建通用语言来解决许多领域中的问题。该域也可以是业务领域。业务领域的一些示例包括:
- 大型保险企业内部开发的针对人寿保险政策的特定领域语言
- 用于战场模拟的领域特定语言
- 用于薪资计算的特定领域语言
- 特定领域的计费语言
领域特定的语言介于小型编程语言和脚本语言之间,并且通常以类似于编程库的方式使用。这些概念之间的界限非常模糊,就像脚本语言和通用语言之间的界限一样。
DSL相关概念
领域建模(也称领域分析):就是要识别出领域中所有的重要元素以及它们之间的协作关系。
问题域:在领域建模活动中,问题域 指构成你所分析业务的那些过程、实体和约束条件。
解答域:问题域的分析模型是用解答域提供的工具和手段实现出来的。
从问题域映射到解答域的实现模型就是DSL(Domain-Specific Language,领域专用语言)的基本思路。
问题域的实体和协作关系必须映射成解答域中相应的制品。图中左边的实体(证券、交易、结算等)需要在右边能找到对应的表示
共通语言:开发团队用共通语汇来表述程序模块,那么产生的代码也将使用同一种领域语言。
在问题域与解答域之间发展出一套共通语汇是走向解答域的第一步。
问题域和解答域享有共同的语汇,可降低信息传达的困难度。在共通语汇之下,你可以从问题域的制品追踪到它在解答域的相应表示
DSL脚本:脚本将共通语汇联系到解答域的实现模型
DSL脚本将实现模型表示为领域语言。脚本中的用词都出自共通语汇,使用户对语言感觉更自然。
设计得当的DSL应该体现以下三项原则,以便与领域用户更好地“沟通”。
- DSL要为问题域制品提供直接的映射。如果问题域有一个名为Trade的实体,那么DSL脚本就必须包含同样名称同样角色的一个抽象。
- DSL脚本必须使用问题域的共通语汇。这些语汇将成为开发者与业务用户增进交流的催化剂。如图1-3所示,当业务用户与软件中的领域模型交互的时候,DSL脚本就是他们的用户界面。
- DSL脚本必须对底层实现进行抽象。这是抽象设计的一项重要原则,对于DSL的设计同样适用。DSL脚本中不可以出现因为实现细节而引入的非本质复杂性
DSL 分类
最常见的分类方法是按照DSL的实现途径来分类。马丁·福勒曾将DSL分为内部和外部两大类,他的分类法得到了绝大多数业界人士的认可和沿袭。
内部与外部之分取决于DSL是否将一种现存语言作为宿主语言,在其上构建自身的实现。
(1)内部DSL也称内嵌式DSL
因为它们的实现嵌入到宿主语言中,与之合为一体。内部DSL将一种现有编程语言作为宿主语言,基于其设施建立专门面向特定领域的各种语义。
(2)外部DSL也称独立DSL
因为它们是从零开始建立起来的独立语言,而不基于任何现有宿主语言的设施建立。外部DSL是从零开发的DSL,在词法分析、解析技术、解释、编译、代码生成等方面拥有独立的设施。开发外部DSL近似于从零开始实现一种拥有独特语法和语义的全新语言。构建工具make 、语法分析器生成工具YACC、词法分析工具LEX等都是常见的外部DSL。
内部 DSL
采用Java语言示例内部DSL实现过程(示例来源:《领域专用语言实战》)
把“交易单定价”的契约实现为一个接口
public interface OrderValuer {
int valueAs(int qty, int unitPrice);
}
DSL的用户针对特定定价策略分别定义该接口的具体实现
public class StandardOrderValuer implements OrderValuer {
public int valueAs(int qty, int unitPrice) {
return unitPrice * qty;
}
}
建立交易单抽象Order
@Data
public class Order {
static class Builder {
private String security;
private int quantity;
private int limitPrice;
private boolean allOrNone;
private int value;
private String boughtOrSold;
public Builder() {
}
public Builder buy(int quantity, String security) {
this.boughtOrSold = "Bought";
this.quantity = quantity;
this.security = security;
// 用方法链接手法实现的连贯接口
return this;
}
public Builder sell(int quantity, String security) {
this.boughtOrSold = "Sold";
this.quantity = quantity;
this.security = security;
return this;
}
public Builder atLimitPrice(int p) {
this.limitPrice = p;
return this;
}
public Builder allOrNone() {
this.allOrNone = true;
return this;
}
public Builder valueAs(OrderValuer ov) {
this.value = ov.valueAs(quantity, limitPrice);
return this;
}
public Order build() {
return new Order(this);
}
}
private final String security;
private final int quantity;
private final int limitPrice;
private final boolean allOrNone;
private int value;
private final String boughtOrSold;
private Order(Builder b) {
security = b.security;
quantity = b.quantity;
limitPrice = b.limitPrice;
allOrNone = b.allOrNone;
value = b.value;
boughtOrSold = b.boughtOrSold;
}
}
DSL使用示例,通过构建者创建Order对象并完成相关操作;
public class Main {
public static void main(String[] args) {
Order o = new Order.Builder()
.buy(100, "IBM")
.atLimitPrice(300)
.allOrNone()
.valueAs(new StandardOrderValuer())
.build();
System.out.println(o);
}
}
Java DSL的不足和应该归咎的Java语言局限
外部 DSL
回顾外部DSL特色:
- 外部DSL不受限于宿主语言的语法,对用户很友好,尤其是对于不懂宿主语言语法的用户
- 创造外部DSL所需的背景知识要远远少于通用语言的
从外部DSL最简单的实现形式说起。DSL的自定义语法需要有配套的语法分析器。分析引擎首先对输入流进行词法分析,将其转化为可识别的词法单元(token)。词法单元在语法上也称为终结符号,(terminal)。随后这些词法单元作为语法正确的语句,被送入产生式规则(production,rule)进行处理。整个过程如图所示
外部DSL最简单的实现形式。语法分析基础设施包揽了产生目标操作所需的一切事务。DSL脚本的所有处理步骤(词法分析、语法分析、生成AST、生成代码)全部集中在一个构造块中。
如果想要实现自己的语法分析基础设施,绕不开语法分析器内容。较为流行的词法分析器生成器如下
语法分析器、语法分析器生成器,实质是对语言语法的一种抽象。如果我们打算手工编写整个分析器,那么需要做这两件事情:
- 定义语言的BNF语法;
- 编写与该语法对应的语法分析器。
外部DSL的示例
想让 BA 负责流程契约的设计,该流程契约是一个活文档,可以跑测试,而 BA 不熟悉宿主语言。于是,我们设计了一种外部 DSL 来专门描述流程契约,对 BA 非常友好,学习成本也很低(不超过 5 分钟就可以学会),最后发现 BA 很快就广泛使用了起来。外部 DSL 并不一定要定义新文法,我们直接复用了 plantUML 文法,安装该插件可以自动生成序列图。
通过修改外部DSL脚本,完成流程契约设计及测试
对于外部 DSL,需要自己实现一个解析器将 DSL 文法解析成语法树,再根据语法树生成语义模型。
语义模型可以看作领域模型(严格的讲语义模型是领域模型的子集)
DSL & DDD
DDD和DSL的融合有三点:
- 面向领域;
- 模型的组装方式;
- 分层架构演进;
DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。
它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。外部 DSL 就是对领域模型的一种组装方式。