bxb100 / bxb100.github.io

This is my blog
https://blog.tomcat.run
MIT License
0 stars 0 forks source link

Java Fluent API 设计速成 #30

Open bxb100 opened 1 year ago

bxb100 commented 1 year ago

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

bxb100 commented 1 year ago

实现一个简单的加减乘除的 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 的实现类传递值不明确
bxb100 commented 1 year ago

类实现

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;
        };
    }
}