目前,我正在阅读Martin Fowler撰写的有关DSL- 特定于域的语言的精彩书籍。 围绕DSL的嗡嗡声,围绕轻松支持DSL创建的语言,以及DSL的使用使我好奇地了解和了解DSL的这一概念。 到目前为止,这本书的使用经验令人印象深刻。
马丁·福勒(Martin Fowler)在他的书中提到的DSL定义:
特定领域的语言(名词):一种表达能力有限的计算机编程语言,专注于特定领域。
DSL并不是什么新鲜事物,它已经存在了很长时间。 人们使用XML作为DSL的一种形式。 使用XML作为DSL很容易,因为我们有XSD来验证DSL,有解析器来解析DSL,还有XSLT来将DSL转换成其他语言。 而且大多数语言为解析XML和填充其域模型对象提供了很好的支持。 诸如Ruby,Groovy等语言的出现增加了DSL的采用。 例如,使用Ruby编写的Web框架Rails广泛使用DSL。
Martin Fowler在他的书中将DSL分为内部,外部和语言工作台。 当我阅读了内部DSL概念时,我使用Java作为宿主语言在自己的简单DSL上进行了一些尝试。 内部DSL位于宿主语言中,并受宿主语言的语法功能约束。 使用Java作为宿主语言并不能给我真正清晰的DSL,但是我努力使它更接近可以舒适地理解DSL的形式。
我试图创建用于创建图的DSL。 据我所知,输入和表示图形的不同方式是: 邻接表和邻接矩阵 。 我一直发现这很难使用,尤其是在Java等语言中,这些语言没有作为一等公民的矩阵。 在这里,我试图创建一个内部DSL,以用Java填充图形。
马丁·福勒(Martin Fowler)在他的书中强调,需要保持语义模型与DSL不同,并引入一种中间表达构建器,该构建器可以从DSL填充语义模型。 通过保持这种状态,我能够通过编写不同的DSL语法和表达式构建器并同时使用相同的语义模型来实现3种不同形式的DSL。
了解语义模型
在这种情况下,语义模型是Graph
类,它包含Edge
实例的列表,每个Edge
包含从Vertex
到Vertex
以及权重。 让我们看一下相同的代码:
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;
}
}
顶点
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;
}
}
现在我们已经有了语义模型,让我们构建DLS。 您应该注意到,我不会更改语义模型。 语义模型不应该改变,这不是一个硬性规定,相反,可以通过添加用于获取数据或修改数据的新API来发展语义模型。 但是将语义模型紧密绑定到DSL并不是一个好的方法。 将它们分开可以帮助独立测试语义模型和DSL。
Martin Fowler陈述了创建内部DSL的不同方法:
- 方法链接
- 功能顺序
- 嵌套函数
- Lambda表达式/闭包
除了功能序列,我已经在这篇文章中说明了3。 但是我在使用Closures / Lambda表达式时使用了Functional Sequence方法。
通过方法链接实现内部DSL
我设想我的DSL是这样的:
Graph()
.edge()
.from("a")
.to("b")
.weight(12.3)
.edge()
.from("b")
.to("c")
.weight(10.5)
为了实现这种DSL的创建,我们必须编写一个表达式生成器,该表达式生成器允许弹出语义模型并提供能够创建DSL的流畅接口 。
我创建了2个表达式构建器-一个用于构建完整的Graph,另一个用于构建单个边。 在构建Graph / Edge的过程中,这些表达式构建器始终保存中间的Graph / Edge对象。 通过在这些表达式构建器中创建静态方法,然后使用静态导入在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
您是否发现此方法比“邻接表/邻接矩阵”方法更易于阅读和理解? 该方法链接类似于我之前写的Train Wreck模式 。
嵌套功能的内部DSL
在嵌套函数方法中,DSL的样式不同。 在这种方法中,我会将函数嵌套在函数中以填充语义模型。 就像是:
Graph(
edge(from("a"), to("b"), weight(12.3),
edge(from("b"), to("c"), weight(10.5)
);
这种方法的优点是它的层次结构自然不同于方法链接,在方法链中,我不得不以不同的方式设置代码格式。 而且这种方法不会在“表达式”构建器中保持任何中间状态,即,在解析/执行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使用了不同的软件包。 因此,请根据您使用的软件包名称更新导入。
//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表达式支持。
使用Lambda表达式的内部DSL
如果您想知道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 expression / closure / block,然后通过执行lambda expression / closure / block填充语义模型。 此实现中的表达式生成器以通过方法链接在DSL实现中相同的方式维护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种不同的方法。
总结一下: