Skip to content
Go back

Java Fluent API 设计速成

Updated:
Edit page

https://blog.jooq.org/the-java-fluent-api-designer-crash-course/

自从 Martin Fowler 谈论流畅的接口以来,人们开始到处链式方法,为每个可能的用例创建流畅的 API(或 DSLs)。原则上,几乎所有类型的 DSL 都可以映射到 Java。让我们看看如何做到这一点

DSL 规则

DSL(领域特定语言)通常是根据大致如下的规则构建的

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD ]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

或者,你也可以像这样声明语法(由 Railroad Diagrams 站点 支持)

Grammar ::= (
  'SINGLE-WORD' |
  'PARAMETERISED-WORD' '('[A-Z]+')' |
  'WORD1' 'OPTIONAL-WORD'? |
  'WORD2' ( 'WORD-CHOICE-A' | 'WORD-CHOICE-B' ) |
  'WORD3'+
)

换句话说,你有一个开始条件或状态,在达到结束条件或状态之前,你可以从中选择一些你的语言的单词。它就像一个状态机,因此可以画成这样的图:

image
用 http://www.bottlecaps.de/rr/ui 创建

Java 实现这些规则

使用 Java 接口,对上述 DSL 进行建模非常简单。本质上,你必须遵循以下转换规则:

请注意,也可以使用类而不是接口对上述 DSL 进行建模。但是一旦你想重用相似的关键字,方法的多重继承可能会派上用场,你可能最好使用接口。

设置好这些规则后,您可以随意重复它们以创建任意复杂度的 DSL,例如 jOOQ。当然,您必须以某种方式实现所有接口,但那是另一回事了。

以下是将上述规则转换为 Java 的方式:

// 初始接口,DSL 的入口点
// 根据DSL的性质,这也可以是一个带有静态方法的类
// 这样可以让你的 DSL 更加扁平(fluent)
interface Start {
	End singleWord();
	End parameterisedWord(String parameter);
	Intermediate1 word1();
	Intermediate2 word2();
	Intermediate3 word3();
}

// 终止接口,也可以包含方法 execute();
interface End {
	void end();
}
// 拓展中间 DSL "step" 可以由方法 optionalWord() 返回
// 让这个方法 "optional"
interface Intermediate1 extends End {
	End optionalWord();
}

// 中间 DSL "step" 提供了几个选择(类似于 Start)
interface Intermediate2 {
	End wordChoiceA();
	End wordChoiceB();
}

// 中间接口由 word3() 返回自己, 以允许重复调用.
// 重复调用可以在任意时间结束, 因为它继承自 End
interface Intermediate3 extends End {
	Intermediate3 word3();
}

定义了上述语法后,我们现在可以直接在 Java 中使用此 DSL。以下是所有可能的结构:


Start start = new StartImpl();// ...

start.singleWord().end();
start.parameterisedWord("abc").end();

start.word1().end();
start.word1().optionalWord().end();

start.word2().wordChoiceA().end();
start.word2().wordChoiceB().end();

start.word3().end();
start.word3().word3().end();
start.word3().word3().word3().end();

最棒的是,您的 DSL 直接用 Java 编译!你得到一个免费的解析器。您还可以在 Scala(或 Groovy)中使用相同的表示法或在 Scala 中使用略有不同的表示法(省略点 . 和括号 ())重新使用此 DSL:

val start = // ... 

(start singleWord) end;
(start parameterisedWord "abc") end; 

(start word1) end;
((start word1) optionalWord) end; 

((start word2) wordChoiceA) end;
((start word2) wordChoiceB) end;

(start word3) end;
((start word3) word3) end;
(((start word3) word3) word3) end;

实例

在 jOOQ 文档和代码库中可以看到一些真实世界的例子。这是从以前的一篇文章中摘录的使用 jOOQ 创建的相当复杂的 SQL 查询:

create().select(
	r1.ROUTINE_NAME,
	r1.SPECIFIC_NAME,
	decode()
			.when(exists(create()
				.selectOne()
				.from(PARAMETERS)
				.where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
				.and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
				.and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
					val("void"))
			.otherwise(r1.DATA_TYPE).as("data_type"),
	r1.NUMERIC_PRECISION,
	r1.NUMERIC_SCALE,
	r1.TYPE_UDT_NAME,
	decode().when(
	exists(
		create().selectOne()
				.from(r2)
				.where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
				.and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
				.and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
		create().select(count())
				.from(r2)
				.where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
				.and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
				.and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
	.as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

这有另一个例子,看起来对我很有吸引力。它称为 jRTF,用于以 fluent 的风格在 Java 中创建 RTF 文档:

rtf()
	.header(
			color( 0xff, 0, 0 ).at( 0 ),
			color( 0, 0xff, 0 ).at( 1 ),
			color( 0, 0, 0xff ).at( 2 ),
			font( "Calibri" ).at( 0 ) )
	.section(
			p( font( 1, "Second paragraph" ) ),
			p( color( 1, "green" ) )
	)
).out( out );

总结

在过去的 7 年里,Fluent API 一直是一种炒作。 Martin Fowler 已经成为一个被大量引用的人并获得了大部分的荣誉,即使以前有 fluent APIs。在 java.lang.StringBuffer 中可以看到 Java 最古老的 fluent API 之一,它允许将任意对象附加到字符串。但是 fluent API 的最大好处是它能够轻松地将 external DSL 映射到 Java 并将它们实现为任意复杂度的 internal DSL


实现一个简单的加减乘除的 Fluent DSL

DSL:

Calc::= Left (Op Right)*

Left::= '(' int | Calc ')'

Right::= '(' int | Calc ')'

Op::= (add | sub | mul | div)
public interface Start {

	Operation left(int left);

	Operation left(End left);

}
public interface Operation {

	Intermediate add();

	Intermediate sub();

	Intermediate mul();

	Intermediate div();
}
public interface Intermediate {

	End right(int right);

	End right(End right);
}
public interface End extends Operation {
	int end();
}

public class Calc {
	public static void main(String[] args) {

		Start start = new StartImpl();

		System.out.println(start.left(1).add().right(2).end());

		System.out.println(start.left(
				start.left(1).mul().right(2)
		).add().right(3).end());

		System.out.println(start.left(1).add().right(
				start.left(2).mul().right(3)
		).end());

		System.out.println(start.left(1).add().right(2).mul().right(1).end());

	}
}

不足的地方:

  1. 这样实现的话, 远算顺序只能从左到右, (a+b)*c 没实现 a+b*c 只能有 a+(b*c)
  2. 重复的 DSL 的实现类传递值不明确

类实现

public class StartImpl implements Start {

	@Override
	public Operation left(int left) {
		return new OperationImpl(left);
	}

	@Override
	public Operation left(End left) {
		return new OperationImpl(left.end());
	}
}
public class OperationImpl implements Operation {

	int left;

	public OperationImpl(int left) {
		this.left = left;
	}

	public OperationImpl() {
	}

	@Override
	public Intermediate add() {

		return new IntermediateImpl(this, OperatorType.ADD);
	}

	@Override
	public Intermediate sub() {

		return new IntermediateImpl(this, OperatorType.SUB);
	}

	@Override
	public Intermediate mul() {

		return new IntermediateImpl(this, OperatorType.MUL);
	}

	@Override
	public Intermediate div() {

		return new IntermediateImpl(this, OperatorType.DIV);
	}
}
public enum OperatorType {
	ADD, SUB, MUL, DIV
}
public class IntermediateImpl implements Intermediate {

	OperatorType type;
	OperationImpl operation;

	public IntermediateImpl(OperationImpl operation, OperatorType type) {
		this.operation = operation;
		this.type = type;
	}

	@Override
	public End right(int right) {
		return new EndImpl(this, right);
	}

	@Override
	public End right(End right) {
		return new EndImpl(this, right.end());
	}
}
public class EndImpl extends OperationImpl implements End {

	private final int right;
	private final int left;
	private final OperatorType op;

	public EndImpl(IntermediateImpl intermediate, int right) {
		if (intermediate.operation instanceof End) {
			this.left = ((End) intermediate.operation).end();
		} else {
			this.left = intermediate.operation.left;
		}
		this.right = right;
		this.op = intermediate.type;
	}

	@Override
	public int end() {
		return switch (this.op) {
			case ADD -> this.left + this.right;
			case SUB -> this.left - this.right;
			case MUL -> this.left * this.right;
			case DIV -> this.left / this.right;
		};
	}
}

Edit page
Share this post on:

Previous Post
Java generics with overloading
Next Post
使用 N_m3u8DL-CLI 简化下载 m3u8 流程