В настоящее время я читаю эту замечательную книгу Мартина Фаулера о 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.0B to C with weight 20.0D to E with weight 50.5Vertices...D E W Y Edges...W to Y with weight 23.0D to E with weight 34.5E 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 builderimport 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.4B to C with weight 56.7D to E with weight 10.4E 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 givenimport 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.4C to D with weight 13.4 |
На этом я заканчиваю этот код тяжелым постом. Дайте мне знать, если вы хотите, чтобы я разделил это на 3 сообщения — по одному для каждой реализации DSL. Я держал его в одном месте, чтобы он помог нам сравнить 3 разных подхода.
Чтобы подвести итог:
- В этом посте я говорил о DSL, внутреннем DSL, как упоминалось в книге Мартина Фаулера « Специфичные для домена языки ».
- Для каждого из трех подходов к реализации Внутренних DSL предусмотрены следующие реализации: