DDD:DSL(领域专用语言)

领域驱动设计系列

Posted by Shoukai Huang on December 21, 2019

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特色:

  1. 外部DSL不受限于宿主语言的语法,对用户很友好,尤其是对于不懂宿主语言语法的用户
  2. 创造外部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 就是对领域模型的一种组装方式。

参考