欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java8采用Martin Fowler的方法创建内部DSL

程序员文章站 2022-04-01 19:57:13
...

本文由 ImportNew - 赖 信涛 翻译自 javacodegeeks。欢迎加入翻译小组。转载请见文末要求。

我最近在读Martin Flower写的一本非常棒的关于DSLs(Domain Specific Languages)的书。围绕DSLs及语言的内容使得我们可以很方便地创建DSLs,DSLs的使用让我对DSLs的概念更加好奇,这本书让人印象深刻。在Martin Fowler一书的开始是这样定义DSLs的:

Domain-specific language (noun): 一种专注于某一领域,仅针对部分表达方式的计算机编程语言。(译者注:求专不求全。)

DSL不是什么新鲜玩意,很久以前,人们就将XML作为一种DSL的一种形式来使用了。将XML作为DSL来使用非常便捷,因为我们可用来检查DSL的XSD,有解释DSL的解释器,还有能将DSL转换成其他语言的XSLT。并且,多数的语言都对解释XML和获取该语言领域中模型对象的内容提供了很好的支持。像是Ruby,Groovy等等这些语言的出现使得DSL被更广泛的接受。比如Rails,一个使用Ruby写的Web框架,广泛地采用了DSLs。

Martin Fowler在他的书中将DSLs分为三类:内部的DSL,外部的DSL,和语言工作平台的DSL。当我读到内部DSL概念时,使用Java作为宿主语言,用我自己简单的DSL小试牛刀了一下。内部DSLs驻于宿主语言中,并且遵守宿主语言的语法。尽管使用Java作为宿主语言没有让我非常清楚的了解DSL,但却有效地让我以一种合适的方式来了解DSL。

我打算写一个能产生图表的DSL。在Java中有两种接收输入并产生表格的方法:Adjacency ListAdjacency Matrix。然而我发现,在没有将矩阵作为“一等公民”提供支持的语言(尤其是Java)实现是非常困难的,所以,我就尝试着在Java中写一个内部DSL来实现对表格的操作。

在他的书中,Martin Fowler强调需要保持语义模型不同于DSL,并且引入了一个中间表达式构造器来从DSL中产生语义模型。所以为了遵守上述内容,我通过写不同的DSL语法和表达式构造器实现了三种不同的DSLs形式,同时又使用了相同的语义模型。

理解语义模型

在这种情景下,语义模型就是包含了Edge实例数组的Graph类,每一个Edge对象又存放了从Vertex到Vertex的数据和一个weight。下面看一下代码吧。

Graph.java

 

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
 
public class Graph {
 
  private List<Edge> edges;
  private Set<Vertex> vertices;
 
  public Graph() {
edges = new ArrayList<>();
vertices = new TreeSet<>();
  }
  public void addEdge(Edge edge){
getEdges().add(edge);
  }
 
  public void addVertice(Vertex v){
getVertices().add(v);
  }
 
  public List<Edge> getEdges() {
return edges;
  }
 
  public Set<Vertex> getVertices() {
return vertices;
  }
 
  public static void printGraph(Graph g){
System.out.println("Vertices...");
for (Vertex v : g.getVertices()) {
  System.out.print(v.getLabel() + " ");
}
System.out.println("");
System.out.println("Edges...");
for (Edge e : g.getEdges()) {
  System.out.println(e);
}
  }
}

 

 

Edge.java

 

public class Edge {
  private Vertex fromVertex;
  private Vertex toVertex;
  private Double weight;
 
  public Edge() {
  }
 
  public Edge(Vertex fromVertex, Vertex toVertex, Double weight) {
this.fromVertex = fromVertex;
this.toVertex = toVertex;
this.weight = weight;
  }
 
  @Override
  public String toString() {
  return fromVertex.getLabel()+" to "+
 toVertex.getLabel()+" with weight "+ 
 getWeight();
  }
 
  public Vertex getFromVertex() {
return fromVertex;
  }
 
  public void setFromVertex(Vertex fromVertex) {
this.fromVertex = fromVertex;
  }
 
  public Vertex getToVertex() {
return toVertex;
  }
 
  public void setToVertex(Vertex toVertex) {
this.toVertex = toVertex;
  }
 
  public Double getWeight() {
return weight;
  }
 
  public void setWeight(Double weight) {
this.weight = weight;
  }
}

 

 

Vertex.java

 

public class Vertex implements Comparable<Vertex> {
  private String label;
 
  public Vertex(String label) {
this.label = label.toUpperCase();
  }
 
  @Override
  public int compareTo(Vertex o) {
return (this.getLabel().compareTo(o.getLabel()));
  }
 
  public String getLabel() {
return label;
  }
 
  public void setLabel(String label) {
this.label = label;
  }
}

 好了,既然语义模型已经到位,我们就开始做DSLs吧。你应该已经注意到了吧,我不打算修改我的语义模型。没有硬性规定语义模型不可以修改,相反,随着新的可以读取和修改数据的API的加入,语义模型可以不断被完善。但是将语义模型和DSL绑定的太死并不可取。保持它们相对分离可以独立地测试语义模型和DSL。

 

Martin Fowler陈述的创建内部DSLs 的方法是:

  • 方法链
  • 功能序列
  • 嵌套函数
  • Lambda表达式/闭包

除了功能序列之外,我在这篇文章中图文并茂的介绍了其中的3种。但是我也在Lambda表达式/闭包中使用了功能序列的方法。

用方法链创建的DSL

我幻想出一种像这样子的DSL:

 

Graph()
  .edge()
.from("a")
.to("b")
.weight(12.3)
  .edge()
.from("b")
.to("c")
.weight(10.5)

 为了能够实现这样的DSL,我们要写一个表达式构造器,能够产生语义模型,提供能产生DSL的流接口

 

我写了来年改革表达式构造器——一个用来完成图表,另一个用来建立每一个的边界。这些图表和边界建立的时候,这些表达式构造器就持有中间的图表和边界对象。以上的语法可以在表达式构造器的静态方法中实现,然后用静态导入,就可以在DSL中使用了。

Graph()方法开始生成Graph模型,同时edge()和一系列方法,也就是from(),to(),weight()产生Edge模型,edge()同时也产生Graph模型。

让我们来看一下GraphBuilder(生成Graph模型的表达式构造器)吧:

GraphBuilder.java

 

public class GraphBuilder {
 
  private Graph graph;
 
  public GraphBuilder() {
graph = new Graph();
  }
 
  //Start the Graph DSL with this method.
  public static GraphBuilder Graph(){
return new GraphBuilder();
  }
 
  //Start the edge building with this method.
  public EdgeBuilder edge(){
EdgeBuilder builder = new EdgeBuilder(this);
 
getGraph().addEdge(builder.edge);
 
return builder;
  }
 
  public Graph getGraph() {
return graph;
  }
 
  public void printGraph(){
Graph.printGraph(graph);
  }
}

 接下来是EdgeBuilder(生成Edge模型的表达式构造器):

 

EdgeBuilder.java

 

public class EdgeBuilder {
 
  Edge edge;
 
  //Keep a back reference to the Graph Builder.
  GraphBuilder gBuilder;
 
  public EdgeBuilder(GraphBuilder gBuilder) {
this.gBuilder = gBuilder;
edge = new Edge();
  }
 
  public EdgeBuilder from(String lbl){
Vertex v = new Vertex(lbl);
edge.setFromVertex(v);
gBuilder.getGraph().addVertice(v);
return this;
  }
  public EdgeBuilder to(String lbl){
Vertex v = new Vertex(lbl);
edge.setToVertex(v);
gBuilder.getGraph().addVertice(v);
return this;
  }
 
  public GraphBuilder weight(Double d){
edge.setWeight(d);
return gBuilder;
  }
 
}

 让我们来试一下这个DSL吧:

 

 

public class GraphDslSample {
 
  public static void main(String[] args) {
 
Graph()
  .edge()
.from("a")
.to("b")
.weight(40.0)
  .edge()
.from("b")
.to("c")
.weight(20.0)
  .edge()
.from("d")
.to("e")
.weight(50.5)
  .printGraph();
 
Graph()
  .edge()
.from("w")
.to("y")
.weight(23.0)
  .edge()
.from("d")
.to("e")
.weight(34.5)
  .edge()
.from("e")
.to("y")
.weight(50.5)
  .printGraph();
 
  }
}

 输出结果是:

 

 

Vertices...
A B C D E 
Edges...
A to B with weight 40.0
B to C with weight 20.0
D to E with weight 50.5
Vertices...
D E W Y 
Edges...
W to Y with weight 23.0
D to E with weight 34.5
E to Y with weight 50.5

 这个方法不是比Adjacency List或者Adjacency Matrix方法更具有可读性吗?这个方法链和我之前写的Train Wreck pattern很像。

 

用嵌套函数创建的DSL

在DSL中使用嵌套函数的风格会有所不同。在这中方法中,我将会在函数之中嵌套函数,来写我的语义模型,向下面这样:

 

Graph(
  edge(from("a"), to("b"), weight(12.3),
  edge(from("b"), to("c"), weight(10.5)
);

 这种方法的好处是,它的层次天生juice不像访法链那样必须用另一种格式来写代码。而且,这种方法不需要在表达式构造器中提供中间变量,也就是说,当DSL被解析或者执行的时候,表达式构造器不需要持有Graph和Edge对象。语义模型和上文中谈到的相同。

 

以下是DSL的表达式构造器。

NestedGraphBuilder.java

 

//Populates the Graph model.
public class NestedGraphBuilder {
 
  public static Graph Graph(Edge... edges){
Graph g = new Graph();
for(Edge e : edges){
  g.addEdge(e);
  g.addVertice(e.getFromVertex());
  g.addVertice(e.getToVertex());
}
return g;
  }
 
}

 

 

NestedEdgeBuilder.java

 

//Populates the Edge model.
public class NestedEdgeBuilder {
 
  public static Edge edge(Vertex from, Vertex to, 
  Double weight){
return new Edge(from, to, weight);
  }
 
  public static Double weight(Double value){
return value;
  }
 
}

 

 

NestedVertexBuilder.java

 

//Populates the Vertex model.
public class NestedVertexBuilder {
  public static Vertex from(String lbl){
return new Vertex(lbl);
  }
 
  public static Vertex to(String lbl){
return new Vertex(lbl);
  }
}

 如果你想严格遵守规则,让所有表达式构造器定义在静态上,我们可以使用静态导入的方法创建一个DSL。

 

注意:表达式构造器、语义模型和DSL分别在不同的包中,所以请根据您的包名更新一下import。

 

//Update this according to the package name of your builder
import static nestedfunction.NestedEdgeBuilder.*;
import static nestedfunction.NestedGraphBuilder.*;
import static nestedfunction.NestedVertexBuilder.*;
 
/**
 *
 * @author msanaull
 */
public class NestedGraphDsl {
 
  public static void main(String[] args) {
Graph.printGraph(
  Graph(
edge(from("a"), to("b"), weight(23.4)),
edge(from("b"), to("c"), weight(56.7)),
edge(from("d"), to("e"), weight(10.4)),
edge(from("e"), to("a"), weight(45.9))
  )
);
 
  }
}

 输出如下:

 

 

Vertices...
A B C D E 
Edges...
A to B with weight 23.4
B to C with weight 56.7
D to E with weight 10.4
E to A with weight 45.9

 有趣的部分来了:我们如何利用DSL支持的lambda表达式呢?

 

在内部DSL中使用lambda表达式

如果你还不知道lambda表达式在Java中能做什么的话,请先阅读本文有关语义模型的部分。

在这个例子中我们会继续使用上面描述的语义模型。这个DSL使用了支持lambda表达式的功能序列。让我们看一下最后的DSL是什么样子的吧:

 

Graph(g -> {
g.edge( e -> {
  e.from("a");
  e.to("b");
  e.weight(12.3);
});
 
g.edge( e -> {
  e.from("b");
  e.to("c");
  e.weight(10.5);
});
 
  }
)

 是的,我知道上面的这个DSL重载了操作符,但是我们不得不这样做。如果你不喜欢,那么可以选择另一种语言。

 

在这个方法中,我们的表达式构造器应该接受lambda表达式/closure/block,然后在lambda表达式/closure/block的基础上创建语义模型。这样实现的语义模型保留了Graph和Edge对象这样的中间值,就想我们在方法链中做的那样。

看一下我们的表达式构造器吧:

GraphBuilder.java

 

//Populates the Graph model.
public class GraphBuilder {
 
  Graph g;
  public GraphBuilder() {
g = new Graph();
  }
 
  public static Graph Graph(Consumer<GraphBuilder> gConsumer){
GraphBuilder gBuilder = new GraphBuilder();
gConsumer.accept(gBuilder);
return gBuilder.g;
  }
 
  public void edge(Consumer<EdgeBuilder> eConsumer){
EdgeBuilder eBuilder = new EdgeBuilder();
eConsumer.accept(eBuilder);
Edge e = eBuilder.edge();
g.addEdge(e);
g.addVertice(e.getFromVertex());
g.addVertice(e.getToVertex());
  }
}

 

 

EdgeBuilder.java

//Populates the Edge model.
public class EdgeBuilder {
  private Edge e;
  public EdgeBuilder() {
    e = new Edge();
  }
 
  public Edge edge(){
    return e;
  }

  public void from(String lbl){
    e.setFromVertex(new Vertex(lbl));
  }
  public void to(String lbl){
    e.setToVertex(new Vertex(lbl));
  }
  public void weight(Double w){
    e.setWeight(w);
  }
}

 

 在GraphBuilder中有两行高亮的代码。这个地方用了Java 8引入的功能接口Consumer.

 

下面我们使用上面的表达式构造器来创建我们的DSL。

 

//Update the package names with the ones you have given
import graph.Graph;
import static builder.GraphBuilder.*;
 
public class LambdaDslDemo {
  public static void main(String[] args) {
Graph g1 = Graph( g -> {
  g.edge( e -> {
e.from("a");
e.to("b");
e.weight(12.4);
  });
 
  g.edge( e -> {
e.from("c");
e.to("d");
e.weight(13.4);
  });
});
 
Graph.printGraph(g1);
  }
}

 输出如下:

 

 

Vertices...
A B C D 
Edges...
A to B with weight 12.4
C to D with weight 13.4

 当我要结束这篇长长的文章的时候,我知道你可能想让我把它分成3个部分每部分介绍一种DSL的实现来发布。我坚持发布在一篇文章中是因为这样可以比较一下这3种方法。

 

概括:

  • 这篇文章中我们讨论了Martin Fowler的Domain Specific Languages一书中涉及的DSL,内部DSL。
  • 分别介绍了三种背部DSL的实现方法
    • 方法链
    • 嵌套函数
    • 支持功能序列的Lambda表达式

原文链接: javacodegeeks 翻译: ImportNew.com 赖 信涛
译文链接: http://www.importnew.com/10907.html
转载请保留原文出处、译者和译文链接。]

  • dsl.rar (22.7 KB)
  • 下载次数: 0
相关标签: java dsl dsl