写点什么

使用 Java 实现内部领域特定语言

  • 2008-03-12
  • 本文字数:10756 字

    阅读完需:约 35 分钟

简介

领域特定语言(DSL)通常被定义为一种特别针对某类特殊问题的计算机语言,它不打算解决其领域外的问题。对于 DSL 的正式研究已经持续很多年,直到最近,在程序员试图采用最易读并且简炼的方法来解决他们的问题的时候,内部 DSL 意外地被写入程序中。近来,随着关于 Ruby 和其他一些动态语言的出现,程序员对 DSL 的兴趣越来越浓。这些结构松散的语言给 DSL 提供某种方法,使得 DSL 允许最少的语法以及对某种特殊语言最直接的表现。但是,放弃编译器和使用类似 Eclipse 这样最强大的现代集成开发环境无疑是该方式的一大缺点。然而,作者终于成功地找到了这两个方法的折衷解决方式,并且,他们将证明该折衷方法不但可能,而且对于使用 Java 这样的结构性语言从面向 DSL 的方式来设计 API 很有帮助。本文将描述怎样使用 Java 语言来编写领域特定语言,并将建议一些组建 DSL 语言时可采用的模式。

Java 适合用来创建内部领域特定语言吗?

在我们审视 Java 语言是否可以作为创建 DSL 的工具之前,我们首先需要引进“内部 DSL”这个概念。一个内部 DSL 在由应用软件的主编程语言创建,对定制编译器和解析器的创建(和维护)都没有任何要求。Martin Fowler 曾编写过大量各种类型的 DSL,无论是内部的还是外部的,每种类型他都编写过一些不错的例子。但使用像 Java 这样的语言来创建 DSL,他却仅仅一笔带过。

另外还要着重提出的很重要的一点是,在 DSL 和 API 两者间其实很难区分。在内部 DSL 的例子中,他们本质上几乎是一样的。在联想到 DSL 这个词汇的时候,我们其实是在利用主编程语言在有限的范围内创建易读的 API。“内部 DSL”几乎是一个特定领域内针对特定问题而创建的极具可读性的 API 的代名词。

任何内部 DSL 都受它基础语言的文法结构的限制。比如在使用 Java 的情况下,大括弧,小括弧和分号的使用是必须的,并且缺少闭包和元编程有可能会导致 DSL 比使用动态语言创建来的更冗长。

但从光明的一面来看,通过使用 Java,我们同时能利用强大且成熟的类似于 Eclipse 和 IntelliJ IDEA 的集成开发环境,由于这些集成开发环境“自动完成(auto-complete)”、自动重构和 debug 等特性,使得 DSL 的创建、使用和维护来的更加简单。另外,Java5 中的一些新特性(比如 generic、varargs 和 static imports)可以帮助我们创建比以往任何版本任何语言都简洁的 API。

一般来说,使用 Java 编写的 DSL 不会造就一门业务用户可以上手的语言,而会是一种业务用户也会觉得易读的语言,同时,从程序员的角度,它也会是一种阅读和编写都很直接的语言。和外部 DSL 或由动态语言编写的 DSL 相比有优势,那就是编译器可以增强纠错能力并标识不合适的使用,而 Ruby 或 Pearl 会“愉快接受”荒谬的 input 并在运行时失败。这可以大大减少冗长的测试,并极大地提高应用程序的质量。然而,以这样的方式利用编译器来提高质量是一门艺术,目前,很多程序员都在为尽力满足编译器而非利用它来创建一种使用语法来增强语义的语言。

利用 Java 来创建 DSL 有利有弊。最终,你的业务需求和你所工作的环境将决定这个选择正确与否。

将 Java 作为内部 DSL 的平台

动态构建 SQL 是一个很好的例子,其建造了一个 DSL 以适合 SQL 领域,获得了引人注意的优势。

传统的使用 SQL 的 Java 代码一般类似于:

String sql = "select id, name " +<br></br> "from customers c, order o " +<br></br> "where " +<br></br> "c.since >= sysdate - 30 and " +<br></br> "sum(o.total) > " + significantTotal + " and " +<br></br> "c.id = o.customer_id and " +<br></br> "nvl(c.status, 'DROPPED') != 'DROPPED'";从作者最近工作的系统中摘录的另一个表达方式是:

Table c = CUSTOMER.alias();<br></br>Table o = ORDER.alias();<br></br>Clause recent = c.SINCE.laterThan(daysEarlier(30));<br></br>Clause hasSignificantOrders = o.TOTAT.sum().isAbove(significantTotal);<br></br>Clause ordersMatch = c.ID.matches(o.CUSTOMER_ID);<br></br>Clause activeCustomer = c.STATUS.isNotNullOr("DROPPED");<br></br>String sql = CUSTOMERS.where(recent.and(hasSignificantOrders)<br></br> .and(ordersMatch)<br></br> .and(activeCustomer)<br></br> .select(c.ID, c.NAME)<br></br> .sql();这个 DSL 版本有几项优点。后者能够透明地适应转换到使用PreparedStatement 的方法——用``String 拼写 SQL 的版本则需要大量的修改才能适应转换到使用捆绑变量的方法。如果引用不正确或者一个 integer 变量被传递到 date column 作比较的话,后者版本根本无法通过编译。代码“nvl(foo, 'X') != 'X'”是 Oracle SQL 中的一种特殊形式,这个句型对于非 Oracle SQL 程序员或不熟悉 SQL 的人来说很难读懂。例如在 SQL Server 方言中,该代码应该这样表达“(foo is null or foo != 'X')”。但通过使用更易理解、更像人类语言的“isNotNullOr(rejectedValue)”来替代这段代码的话,显然会更具阅读性,并且系统也能够受到保护,从而避免将来为了利用另一个数据库供应商的设施而不得不修改最初的代码实现。

使用 Java 创建内部 DSL

创建 DSL 最好的方法是,首先将所需的 API 原型化,然后在基础语言的约束下将它实现。DSL 的实现将会牵涉到连续不断的测试来肯定我们的开发确实瞄准了正确的方向。该“原型-测试”方法正是测试驱动开发模式(TDD-Test-Driven Development)所提倡的。

在使用 Java 来创建 DSL 的时候,我们可能想通过一个连贯接口(fluent interface)来创建 DSL。连贯接口可以对我们所想要建模的领域问题提供一个简介但易读的表示。连贯接口的实现采用方法链接(method chaining)。但有一点很重要,方法链接本身不足以创建 DSL。一个很好的例子是 Java 的StringBuilder,它的方法“append”总是返回一个同样的StringBuilder的实例。这里有一个例子:

StringBuilder b = new StringBuilder();<br></br>b.append("Hello. My name is ")<br></br> .append(name)<br></br> .append(" and my age is ")<br></br> .append(age);该范例并不解决任何领域特定问题。

除了方法链接外,静态工厂方法(static factory method)和 import 对于创建简洁易读的 DSL 来说是不错的助手。在下面的章节中,我们将更详细地讲到这些技术。

1. 方法链接(Method Chaining)

使用方法链接来创建 DSL 有两种方式,这两种方式都涉及到链接中方法的返回值。我们的选择是返回 this 或者返回一个中间对象,这决定于我们试图要所达到的目的。

1.1 返回this

在可以以下列方式来调用链接中方法的时候,我们通常返回this

  • 可选择的
  • 以任何次序调用
  • 可以调用任何次数

我们发现运用这个方法的两个用例:

  1. 相关对象行为链接
  2. 一个对象的简单构造/配置

1.1.1 相关对象行为链接

很多次,我们只在企图减少代码中不必要的文本时,才通过模拟分派“多信息”(或多方法调用)给同一个对象而将对象的方法进行链接。下面的代码段显示的是一个用来测试 Swing GUI 的 API。测试所证实的是,如果一个用户试图不输入她的密码而登录到系统中的话,系统将显示一条错误提示信息。

DialogFixture dialog = new DialogFixture(new LoginDialog());<br></br>dialog.show();<br></br>dialog.maximize();<br></br>TextComponentFixture usernameTextBox = dialog.textBox("username");<br></br>usernameTextBox.clear();<br></br>usernameTextBox.enter("leia.organa");<br></br>dialog.comboBox("role").select("REBEL");<br></br>OptionPaneFixture errorDialog = dialog.optionPane();<br></br>errorDialog.requireError();<br></br>errorDialog.requireMessage("Enter your password");尽管代码很容易读懂,但却很冗长,需要很多键入。

下面列出的是在我们范例中所使用的TextComponentFixture的两个方法:

public void clear() {<br></br> target.setText("");<br></br>}<p>public void enterText(String text) {</p><br></br> robot.enterText(target, text);<br></br>}我们可以仅仅通过返回this来简化我们的测试 API,从而激活方法链接:

public TextComponentFixture clear() {<br></br> target.setText("");<br></br> return this;<br></br>}<p> public TextComponentFixture enterText(String text) {</p><br></br> robot.enterText(target, text);<br></br> return this;<br></br>}在激活所有测试设施中的方法链接之后,我们的测试代码现在缩减到:

DialogFixture dialog = new DialogFixture(new LoginDialog());<br></br>dialog.show().maximize();<br></br>dialog.textBox("username").clear().enter("leia.organa");<br></br>dialog.comboBox("role").select("REBEL");<br></br>dialog.optionPane().requireError().requireMessage("Enter your password");这个结果代码显然更加简洁易读。正如先前所提到的,方法链接本身并不意味着有了 DSL。我们需要将解决领域特定问题的对象的所有相关行为相对应的方法链接起来。在我们的范例中,这个领域特定问题就是 Swing GUI 测试。

1.1.2 对象的简单构造/配置

这个案例和上文的很相似,不同是,我们不再只将一个对象的相关方法链接起来,取而代之的是,我们会通过连贯接口创建一个“builder”来构建和/或配置对象。

下面这个例子采用了 setter 来创建“dream car”:

DreamCar car = new DreamCar();<br></br>car.setColor(RED);<br></br>car.setFuelEfficient(true);<br></br>car.setBrand("Tesla"); ``DreamCar类的代码相当简单:

// package declaration and imports<p>public class DreamCar {</p><p> private Color color;</p><br></br> private String brand;<br></br> private boolean leatherSeats;<br></br> private boolean fuelEfficient;<br></br> private int passengerCount = 2;<p> // getters and setters for each field</p><br></br>}尽管创建DreamCar非常简单,并且代码也十分可读,但我们仍能够使用 car builder 来创造更简明的代码:

// package declaration and imports<p>public class DreamCarBuilder {</p><p> public static DreamCarBuilder car() {</p><br></br> return new DreamCarBuilder();<br></br> }<p> private final DreamCar car;</p><p> private DreamCarBuilder() {</p><br></br> car = new DreamCar();<br></br> }<p> public DreamCar build() { return car; }</p><p> public DreamCarBuilder brand(String brand) {</p><br></br> car.setBrand(brand);<br></br> return this;<br></br> }<p> public DreamCarBuilder fuelEfficient() {</p><br></br> car.setFuelEfficient(true);<br></br> return this;<br></br> }<p> // similar methods to set field values</p><br></br>}通过 builder,我们还能这样重新编写DreamCar的创建过程:

DreamCar car = car().brand("Tesla")<br></br> .color(RED)<br></br> .fuelEfficient()<br></br> .build();使用连贯接口,再一次减少了代码噪音,所带来的结果是更易读的代码。需要指出的很重要的一点是,在返回this的时候,链中任何方法都可以在任何时候被调用,并且可以被调用任何次数。在我们的例子中,color这个方法我们可想调用多少次就调用多少次,并且每次调用都会覆盖上一次调用所设置的值,这在应用程序的上下文中可能是合理的。

另一个重要的发现是,没有编译器检查来强制必需的属性值。一个可能的解决方案是,如果任何对象创建和/或配置规则没有得到满足的话(比如,一个必需属性被遗忘),在运行时抛出异常。通过从链中方法返回中间对象有可能达到规则校验的目的。

1.2 返回中间对象

从连贯接口的方法中返回中间对象和返回this的方式相比,有这样一些优点:

  • 我们可以使用编译器来强制业务规则(比如:必需属性)
  • 我们可以通过限制链中下一个元素的可用选项,通过一个特殊途径引导我们的连贯接口用户
  • 在用户可以(或必须)调用哪些方法、调用顺序、用户可以调用多少次等方面,给了 API 创建者更大的控制力

下面的例子表示的是通过带参数的构建函数来创建一个 vacation 对象的实例:

Vacation vacation = new Vacation("10/09/2007", "10/17/2007",<br></br> "Paris", "Hilton",<br></br> "United", "UA-6886");这个方法的好处在于它可以迫使我们的用户申明所有必需的参数。不幸的是,这儿有太多的参数,而且没有表达出他们的目的。“Paris”和“Hilton”所指的分别是目的地的城市和酒店?还是我们同事的名字?:)

第二个方法是将 setter 方法对每个参数进行建档:

Vacation vacation = new Vacation();<br></br> vacation.setStart("10/09/2007");<br></br> vacation.setEnd("10/17/2007");<br></br>vacation.setCity("Paris");<br></br>vacation.setHotel("Hilton");<br></br>vacation.setAirline("United");<br></br>vacation.setFlight("UA-6886");现在我们的代码更易读,但仍然很冗长。第三个方案则是创建一个连贯接口来构建 vacation 对象的实例,如同在前一章节提供的例子一样:

Vacation vacation = vacation().starting("10/09/2007")<br></br> .ending("10/17/2007")<br></br> .city("Paris")<br></br> .hotel("Hilton")<br></br> .airline("United")<br></br> .flight("UA-6886");这个版本的简明和可读性又进了一步,但我们丢失了在第一个版本(使用构建函数的那个版本)中所拥有的关于遗忘属性的校验。换句话说,我们并没有使用编译器来校验可能存在的错误。这时,对这个方法我们所能做的最好的改进是,如果某个必需属性没有设置的话,在运行时抛出异常。

以下是第四个版本,连贯接口更完善的版本。这次,方法返回的是中间对象,而不是this:

Period vacation = from("10/09/2007").to("10/17/2007");<br></br>Booking booking = vacation.book(city("Paris").hotel("Hilton"));<br></br>booking.add(airline("united").flight("UA-6886");这里,我们引进了PeriodBookingLocationBookableItemHotelFlight)、以及 Airline的概念。在这里的上下文中,airline 作为Flight对象的一个工厂;LocationHotel的工厂,等等。我们所想要的 booking 的文法隐含了所有这些对象,几乎可以肯定的是,这些对象在系统中会有许多其他重要的行为。采用中间对象,使得我们可以对用户行为可否的限制进行编译器校验。例如,如果一个 API 的用户试图只通过提供一个开始日期而没有明确结束日期来预定假期的话,代码则不会被编译。正如我们之前提到,我们可以创建一种使用文法来增强语义的语言。

我们在上面的例子中还引入了静态工厂方法的应用。静态工厂方法在与静态 import 同时使用的时候,可以帮助我们创建更简洁的连贯接口。若没有静态 import,上面的例子则需要这样的代码:

Period vacation = Period.from("10/09/2007").to("10/17/2007");<br></br>Booking booking = vacation.book(Location.city("Paris").hotel("Hilton"));<br></br>booking.add(Flight.airline("united").flight("UA-6886");上面的例子不及采用了静态 import 的代码那么易读。在下面的章节中,我们将对静态工厂方法和 import 做更详细的讲解。

这是关于使用 Java 编写 DSL 的第二个例子。这次,我们将 Java reflection 的使用进行简化:

Person person = constructor().withParameterTypes(String.class)<br></br> .in(Person.class)<br></br> .newInstance("Yoda");<p> method("setName").withParameterTypes(String.class)</p><br></br> .in(person)<br></br> .invoke("Luke");<p> field("name").ofType(String.class)</p><br></br> .in(person)<br></br> .set("Anakin");在使用方法链接的时候,我们必须倍加注意。方法链接很容易会被烂用,它会导致许多调用被一起链接在单一行中的“火车残骸”现象。这会引发很多问题,包括可读性的急剧下滑以及异常发生时栈轨迹(stack trace)的含义模糊。

2. 静态工厂方法和 Imports

静态工厂方法和 imports 可以使得 API 更加简洁易读。我们发现,静态工厂方法是在 Java 中模拟命名参数的一个非常方便的方法,是许多程序员希望开发语言中所能够包含的特性。比如,对于这样一段代码,它的目的在于通过模拟一个用户在一个JTable中选择一行来测试 GUI:

dialog.table("results").selectCell(6, 8); // row 6, column 8没有注释“// row 6, column 8”,这段代码想要实现的目的很容易被误解(或者说根本没有办法理解)。我们则需要花一些额外的时间来检查文档或者阅读更多行代码才能理解“6”和“8”分别代表什么。我们也可以将行和列的下标作为变量来声明,而非像上面这段代码那样使用常量:

int row = 6;<br></br>int column = 8;<br></br>dialog.table("results").selectCell(row, column);我们已经改进了这段代码的可读性,但却付出了增加需要维护的代码的代价。为了将代码尽量简化,理想的解决方案是像这样编写代码:

dialog.table("results").selectCell(row: 6, column: 8);不幸的是,我们不能这样做,因为 Java 不支持命名参数。好的一面的是,我们可以通过使用静态工厂方法和静态 imports 来模拟他们,从而可以得到这样的代码:

dialog.table("results").selectCell(row(6).column(8));我们可以从改变方法的签名(signature)开始,通过包含所有参数的对象来替代所有这些参数。在我们的例子中,我们可以将方法selectCell(int, int)修改为:

selectCell(TableCell); ``TableCell will contain the values for the row and column indices:

TableCell将包含行和列的下标值:

public final class TableCell {<p> public final int row;</p><br></br> public final int column;<p> public TableCell(int row, int column) {</p><br></br> this.row = row;<br></br> this.column = column;<br></br> }<br></br>}这时,我们只是将问题转移到了别处:TableCell的构造函数仍然需要两个int值。下一步则是将引入一个TableCell的工厂,这个工厂将对初始版本中selectCell的每个参数设置一个对应的方法。另外,为了迫使用户使用工厂,我们需要将TableCell的构建函数修改为private

public final class TableCell {<p> public static class TableCellBuilder {</p><br></br> private final int row;<p> public TableCellBuilder(int row) {</p><br></br> this.row = row;<br></br> }<p> public TableCell column(int column) {</p><br></br> return new TableCell(row, column);<br></br> }<br></br> }<p> public final int row;</p><br></br> public final int column;<p> private TableCell(int row, int column) {</p><br></br> this.row = row;<br></br> this.column = column;<br></br> }<br></br>}通过TableCellBuilder工厂,我们可以创建对每个参数都有一个调用方法的TableCell。工厂中的每个方法都表达了其参数的目的:

selectCell(new TableCellBuilder(6).column(8));最后一步是引进静态工厂方法来替代TableCellBuilder构造函数的使用,该构造函数没有表达出 6 代表的是什么。如我们在之前所实现的那样,我们需要将构造函数设置为private来迫使用户使用工厂方法:

public final class TableCell {<p> public static class TableCellBuilder {</p><br></br> public static TableCellBuilder row(int row) {<br></br> return new TableCellBuilder(row);<br></br> }<p> private final int row;</p><p> private TableCellBuilder(int row) {</p><br></br> this.row = row;<br></br> }<p> private TableCell column(int column) {</p><br></br> return new TableCell(row, column);<br></br> }<br></br> }<p> public final int row;</p><br></br> public final int column;<p> private TableCell(int row, int column) {</p><br></br> this.row = row;<br></br> this.column = column;<br></br> }<br></br> }现在我们只需要selectCell的调用代码中增加内容,包含对TableCellBuilderrow方法的静态 import。为了刷新一下我们的记忆,这是如何实现调用selectCell的代码:

dialog.table("results").selectCell(row(6).column(8));我们的例子说明,一点点额外的工作可以帮助我们克服主机编程语言中的一些限制。正如之前提到的,这只是我们通过使用静态工厂方法和 imports 来改善代码可读性的很多方法中的一个。下列代码段是以另一种不同的方法利用静态工厂方法和 imports 来解决相同的 table 坐标问题:

/**<br></br> * @author Mark Alexandre<br></br> */<br></br>public final class TableCellIndex {<p> public static final class RowIndex {</p><br></br> final int row;<br></br> RowIndex(int row) {<br></br> this.row = row;<br></br> }<br></br> }<p> public static final class ColumnIndex {</p><br></br> final int column;<br></br> ColumnIndex(int column) {<br></br> this.column = column;<br></br> }<br></br> }<p> public final int row;</p><br></br> public final int column;<br></br> private TableCellIndex(RowIndex rowIndex, ColumnIndex columnIndex) {<br></br> this.row = rowIndex.row;<br></br> this.column = columnIndex.column;<br></br> }<p> public static TableCellIndex cellAt(RowIndex row, ColumnIndex column) {</p><br></br> return new TableCellIndex(row, column);<br></br> }<p> public static TableCellIndex cellAt(ColumnIndex column, RowIndex row) {</p><br></br> return new TableCellIndex(row, column);<br></br> }<p> public static RowIndex row(int index) {</p><br></br> return new RowIndex(index);<br></br> }<p> public static ColumnIndex column(int index) {</p><br></br> return new ColumnIndex(index);<br></br> }<br></br>}这个方案的第二个版本比第一个版本更具灵活性,因为这个版本允许我们通过两种途径来声明行和列的坐标:

dialog.table("results").select(cellAt(row(6), column(8));<br></br>dialog.table("results").select(cellAt(column(3), row(5));## 组织代码

相比返回中间对象的的方式来说,返回this的方式更加容易组织连贯接口的代码。前面的案例中,我们的最后结果是使用更少的类来封装连贯接口的逻辑,并且使得我们可以在组织非 DSL 代码的时候使用同样的规则或约定。

采用中间对象作为返回类型来组织连贯接口的代码更具技巧性,因为我们将连贯接口的逻辑遍布在一些小的类上。由于这些类结合在一起作为整体而形成我们的连贯接口,这使得将他们作为整体对待更为合理,我们可能不想将他们和 DSL 外的其他一些类混淆一起,那么我们有两个选择:

  • 将中间对象作为内嵌类创建
  • 将中间对象至于他们自己的顶级类中,将所有这些中间对象类放入同一个包中

分解我们的系统所采用的方式取决于我们想要实现的文法的几个因素:DSL 的目的,中间对象(如果有的话)的数量和大小(以代码的行数来计),以及 DSL 如何来与其它的代码库及其它的 DSL 相协调。

对代码建档

在组织代码一章节中提到,对方法返回this的连贯接口建档比对返回中间对象的连贯接口建档来的简单的多,尤其是在使用 Javadoc 来建档的情况下。

Javadoc 每次显示一个类的文档,这对于使用中间对象的 DSL 来说可能不是最好的方式:因为这样的 DSL 包含一组类,而不是单个的类。由于我们不能改变 Javadoc 显示我们的 API 文档的方式,我们发现在 package.html 文件中,加入一个使用连贯接口(包含所有相关类)、且对链中每个方法提供链接的例子,可以将 Javadoc 的限制的影响降到最低。

我们需要注意不要创建重复文档,因为那样会增加 API 创建者的维护代价。最好的方法是尽可能依赖于像可执行文档那样的测试。

结论

Java 适用于创建开发人员易读易写的、并且对于商业用户用样易读的内部领域特定语言。用 Java 创建的 DSL 可能比那些由动态语言创建的 DSL 来的冗长。但好的一面是,通过使用 Java,我们可以利用编译器来增强 DSL 的语义。另外,我们依赖于成熟且强大的 Java 集成开发环境,从而使 DSL 的创建、使用和维护更加简单。

使用 Java 创建 DSL 需要 API 设计者做更多的工作,有更多的代码和文档需要创建和维护。但是,付出总有回报。使用我们 API 的用户在他们的代码库中会看到更多的优化。他们的代码将会更加简洁,更易于维护,这些将使得他们的生活更加轻松。

使用 Java 创建 DSL 有很多种不同的方式,这取决于我们试图达到的目的是什么。尽管没有什么通用的方法,我们还是发现结合方法链接和静态工厂方法与 imports 的方式可以得到干净、简洁、易读易写的 API。

总而言之,在使用 Java 来创建 DSL 的时候有利有弊。这都由我们——开发人员根据项目需求去决定它是否是正确的选择。

另外一点题外话, Java 7 可能会包含帮助我们创建不那么冗长的 DSL 的新语言特性(比如闭包)。如果想得到更多关于建议中所提特性的全面的列表,请访问 Alex Miller 的 blog

关于作者

Alex Ruiz 是 Oracle 开发工具组织中的一名软件工程师。Alex 喜欢阅读任何关于 Java、测试、OOP 和 AOP 的信息,他最大的爱好就是编程。在加入 Oracle 之前,Alex 曾是 ThoughtWorks 的咨询顾问。Alex 的 blog 为 http://www.jroller.com/page/alexRuiz

Jeff Bay 是纽约一家对冲基金的高级软件工程师。他曾多次建立高质量、迅速的 XP 团队工作于例如 Onstar 的计划注册系统、租赁软件、web 服务器、建筑项目管理等各种系统。他对于消除重复和防止 bug 方面怀有极大的热情,以提高开发者的工作效率和减少在各种任务上所花费的时间。

相关资料

查看原文: An Approach to Internal Domain-Specific Languages in Java

2008-03-12 01:044934
用户头像

发布了 71 篇内容, 共 20.1 次阅读, 收获喜欢 3 次。

关注

评论

发布
暂无评论
发现更多内容

滚雪球学 Python 番外篇之游戏世界,游戏也有 Hello World

梦想橡皮擦

10月月更

场外OTC交易系统APP开发(案例)

主干开发你必须知道的7件事

华为云开发者联盟

产品 测试 团队 开发 主干开发

Spinnaker:云原生多云环境持续部署的未来

博文视点Broadview

币币撮合交易软件系统开发(源码搭建)

直播预告 | Apache APISIX × Apache SkyWalking 线上分享

API7.ai 技术团队

Apache Skywalking API网关 APISIX Meetup

Vue进阶(幺肆贰):CSS-静态定位,相对定位,绝对定位,固定定位的用法和区别详解

No Silver Bullet

Vue 元素定位 10月月更

边缘AI方案落地问题探讨

华为云开发者联盟

机器学习 AI 算法 边侧数据 边缘云

为金融场景而生的数据类型:Numeric

青云技术社区

postgresql 云计算 源码 云原生

LeaRun.Java可视化流程简单配置过程

雯雯写代码

java

一文读懂「TTS语音合成技术」

澳鹏Appen

人工智能 语音 nlp 语音合成 TTS

谈 C++17 里的 Command 模式

hedzr

设计模式 命令模式 Design Patterns c++17 Command Pattern

广角-聊聊Underlay

Lance

容器 云原生 Underlay

JavaAgent查看动态生成类的源码

长河

【Flutter 专题】24 易忽略的【小而巧】的技术点汇总 (三)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

【LeetCode】最小操作次数使数组元素相等Java题解

Albert

算法 LeetCode 10月月更

场外OTC交易软件系统开发介绍(源码)

数字货币交易软件系统开发介绍(搭建)

Tensorflow Lite移动平台编译|Bazel实践

轻口味

人工智能 tensorflow ios android 10月月更

技术分享| 音视频多频道使用的正确姿势

anyRTC开发者

音视频 WebRTC 实时通信 多频道

新手 Gopher 如何写出更健壮的 Go 代码

baiyutang

golang 10月月更

java.lang.OutOfMemoryError:GC overhead limit exceeded

看山

Java OOM 10月月更

netty系列之:让TLS支持http2

程序那些事

Netty 网络协议 HTTP 程序那些事 http2

带你掌握java反序列化漏洞及其检测

华为云开发者联盟

Java 安全 漏洞

全周期数据管控,为「快递大数据+」保驾护航

BinTools图尔兹

存储大师班 | 浅谈数据保护之快照与备份

QingStor分布式存储

分布式存储 快照 备份

Python代码阅读(第41篇):矩阵转置

Felix

Python 编程 Code Programing 阅读代码

币币交易APP系统开发费用(源码)

英特尔联合阿里巴巴深化从云到端全面技术合作,加速数智中国创新发展

科技新消息

助力建设智慧社区,EMQ 映云科技服务美好生活

EMQ映云科技

物联网 mqtt 智慧社区

华为云GaussDB深耕数字化下半场,持续打造数据库根技术

华为云开发者联盟

Serverless 云原生 华为云 GaussDB 云数据库

使用Java实现内部领域特定语言_Java_Alex Ruiz_InfoQ精选文章