Статьи

Создание внутренних DSL в Java, Java 8 — принятие подхода Мартина Фаулера

В настоящее время я читаю эту замечательную книгу Мартина Фаулера о DSLs — предметно- ориентированных языках . Шумиха вокруг DSL, вокруг языков, которые с легкостью поддерживают создание DSL, использование DSL, мне стало интересно узнать и узнать об этой концепции DSL. И опыт с книгой до сих пор был впечатляющим.

Определение DSL, как указано Мартином Фаулером в его книге:

Специфичный для предметной области язык (существительное): язык программирования с ограниченной выразительностью, ориентированный на конкретную область.

В DSL нет ничего нового, он был там довольно давно. Люди использовали XML как форму DSL. Использовать XML в качестве DSL легко, потому что у нас есть XSD для проверки DSL, у нас есть парсеры для анализа DSL и у нас есть XSLT для преобразования DSL в другие языки. И большинство языков обеспечивают очень хорошую поддержку для синтаксического анализа XML и заполнения их объектов модели домена. Появление таких языков, как Ruby, Groovy и других, расширило использование DSL. Например, Rails, веб-фреймворк, написанный на Ruby, широко использует DSL.

В своей книге Мартин Фаулер классифицирует DSL как внутренние, внешние и языковые рабочие места. Читая концепции внутреннего DSL, я немного поиграл с моим собственным простым DSL, используя Java в качестве основного языка. Внутренние DSL хранятся на языке хоста и связаны синтаксическими возможностями языка хоста. Использование Java в качестве основного языка не дало мне по-настоящему четких DSL, но я постарался приблизить его к форме, в которой я мог бы с комфортом понимать DSL.

Я пытался создать DSL для создания графика. Насколько мне известно, разные способы ввода и представления графика: Список смежности и Матрица смежности . Я всегда находил это трудным для использования, особенно в таких языках, как Java, в которых нет матриц первого класса. И здесь я пытаюсь создать внутренний DSL для заполнения графика в Java.

В своей книге Мартин Фаулер подчеркивает необходимость отличать семантическую модель от DSL и вводить промежуточный построитель выражений, который заполняет семантическую модель из DSL. Поддерживая это, я смог достичь 3 различных форм DSL, написав разные синтаксисы DSL и построители выражений, и в то же время используя одну и ту же семантическую модель.

Понимание семантической модели

Семантическая модель в данном случае — это класс Graph который содержит список экземпляров Edge и каждого Edge содержащего от Vertex до Vertex и веса. Давайте посмотрим на код для того же:

Graph.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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.

Мартин Фаулер заявил, что существуют разные подходы к созданию внутренних DSL:

Я иллюстрировал 3 в этом посте, кроме функциональной последовательности. Но я использовал подход Functional Sequence при использовании выражения Closures / Lambda .

Внутренний DSL методом цепочки

Я предполагаю, что мой DSL будет примерно таким:

1
2
3
4
5
6
7
8
9
Graph()
  .edge()
    .from("a")
    .to("b")
    .weight(12.3)
  .edge()
    .from("b")
    .to("c")
    .weight(10.5)

Чтобы сделать возможным создание такого DSL, нам нужно написать построитель выражений, который позволяет использовать семантическую модель и обеспечивает свободный интерфейс, позволяющий создавать DSL.

Я создал 2 построителя выражений — одно для построения полного графика, а другое для построения отдельных ребер. Все время, пока строится Graph / Edge, эти построители выражений содержат промежуточные объекты Graph / Edge. Вышеупомянутый синтаксис может быть достигнут путем создания статического метода в этих построителях выражений и последующего использования статического импорта для использования их в DSL.

Graph() начинает Graph модель Graph тогда как edge() и ряд методов позже, а именно: from() , to() , weight() заполняет модель Edge . edge() также заполняет модель Graph .

Давайте посмотрим на GraphBuilder, который является конструктором выражений для заполнения модели Graph .

GraphBuilder.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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();
 
  }
}

И вывод будет:

01
02
03
04
05
06
07
08
09
10
11
12
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 отличается. При таком подходе я вложил бы функции в функции, чтобы заполнить мою семантическую модель. Что-то типа:

1
2
3
4
Graph(
  edge(from("a"), to("b"), weight(12.3),
  edge(from("b"), to("c"), weight(10.5)
);

Преимущество этого подхода состоит в том, что его иерархическая структура, естественно, отличается от цепочки методов, где мне приходилось форматировать код другим способом. И этот подход не поддерживает промежуточное состояние в построителях Expression, то есть построители выражений не содержат объекты Graph и Edge во время анализа / выполнения DSL. Семантическая модель остается такой же, как обсуждалось здесь .

Давайте посмотрим на построители выражений для этого DSL.

NestedGraphBuilder.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
//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

01
02
03
04
05
06
07
08
09
10
11
12
13
//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

01
02
03
04
05
06
07
08
09
10
//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. Поэтому, пожалуйста, обновите импорт в соответствии с именами пакетов, которые вы использовали.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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))
      )
    );
 
  }
}

И вывод для этого будет:

1
2
3
4
5
6
7
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.

Внутренний DSL с использованием лямбда-выражения

Если вам интересно, что делают лямбда-выражения в Java, то, пожалуйста, потратьте некоторое время здесь, прежде чем продолжить.

В этом примере мы также будем придерживаться той же семантической модели, описанной здесь . Этот DSL использует Функциональную Последовательность наряду с использованием поддержки лямбда-выражений. Давайте посмотрим, как мы хотим, чтобы наш последний DSL был похож на:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 перегружен пунктуацией, но мы должны жить с этим. Если вам это не нравится, то, возможно, выберите другой язык.

При таком подходе наши конструкторы выражений должны принимать лямбда-выражение / закрытие / блок, а затем заполнять семантическую модель, выполняя лямбда-выражение / закрытие / блок. Построитель выражений в этой реализации поддерживает промежуточное состояние объектов Graph и Edge таким же образом, как мы это делали в реализации DSL с помощью Method Chaining .

Давайте посмотрим на наших строителей выражений:

GraphBuilder.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
//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 вы видите две выделенные строки кода. В них используется функциональный интерфейс Consumer , который будет представлен в Java 8.

Теперь давайте воспользуемся вышеупомянутыми построителями выражений для создания нашего DSL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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);
  }
}

И вывод:

1
2
3
4
5
Vertices...
A B C D
Edges...
A to B with weight 12.4
C to D with weight 13.4

На этом я заканчиваю этот код тяжелым постом. Дайте мне знать, если вы хотите, чтобы я разделил это на 3 сообщения — по одному для каждой реализации DSL. Я держал его в одном месте, чтобы он помог нам сравнить 3 разных подхода.

Чтобы подвести итог: