自定义 Java 查询类 (DSL):生成器模式、静态导入还是用于复杂查询的其他内容?方法和/或/非查询概括

我正在创建一个自定义查询类,我不确定编写它的最优雅方法。

目标是:

  • 简单易用
  • 扩展
  • 灵活,以便可以制定复杂的查询

方法

目前我可以想到两种选择。

1. 生成器模式

Result r = new Query().is("tall").capableOf("basketball").name("michael").build();

这些方法,并返回对该对象的自引用。 将返回一个对象。is()capableOf()name()Querybuild()Result

2. 静态导入

Result r = new Query(is("tall"), capableOf("basketball"), name("michael"));

方法 ,是静态导入和返回对象。Query 构造函数采用任意数量的条件并返回结果。is()capableOf()name()Condition

和/或/非查询

如下所示的更复杂的查询的表述很复杂:

高大的篮球运动员名叫[迈克尔·奥丹尼斯]

联盟

弯曲有光泽的银勺

生成器模式:

Result r = new Query().is("tall").capableOf("basketball").or(new Query().name("michael"), new Query().name("dennis")).
    union(
        new Query().color("silver").a("spoon").is("bent").is("shiny")
    ).
    build();

这很难编写和阅读。另外,我不喜欢多次使用。new

静态导入:

Result r = new Query(is("tall"), capableOf("basketball"), or(name("michael"), name("dennis"))).
    union(color("silver"), a("spoon"), is("bent"), is("shiny"));

对我来说看起来更好,但我真的不喜欢使用静态导入。它们在ide集成,自动完成和文档方面很困难。

概括

我正在寻找一个有效的解决方案,因此我对任何类型的建议都持开放态度。我不仅限于我提出的两种选择,如果有其他可能性,如果你告诉我,我会很高兴。如果您需要更多信息,请通知我。


答案 1

您将要在 Java 中实现域特定语言 (DSL)。有些人会把你的DSL称为“内部”DSL,因为你想使用标准的Java构造,而不是“外部”DSL,后者更强大(SQL,XML,任何类型的协议),但必须使用字符串串联来原始构造。

我们公司维护着jOOQ,它将SQL建模为Java中的“内部”DSL(在其中一条评论中也提到了这一点)。我建议您执行以下步骤:

  1. 意识到你的语言应该是什么样子的。不要马上考虑Java(“内部”DSL)。从你自己的语言(“外部”DSL)的角度来思考。在这一点上,您将在Java中实现它的事实应该并不重要。也许你甚至可以用XML实现它,或者你会为它编写自己的解析器/编译器。在用Java实现之前,首先考虑你的语言规范将使你的DSL更具表现力,更直观,更可扩展。
  2. 一旦你确定了你的语言的一般语法和语义,试着画一个BNF符号你的语言。你不必在开始时过于精确,但这会给它一些正式的方面。铁路图是一个非常好的工具。您将意识到哪些组合是可能的,哪些组合不是。此外,这是创建整体语言文档的好方法,因为单方法Javadocs对新手用户没有太大帮助。
  3. 当您有正式的语法时,请遵循我们在这里的博客中提到的规则:http://blog.jooq.org/2012/01/05/the-java-fluent-api-designer-crash-course。这些规则在设计jOOQ API时非常有用,我们的用户已经报告该API非常直观(如果他们已经知道SQL,那就是)。

我个人对你的建议是:

  1. is、 、 等是谓词工厂方法。静态方法是 Java 中的最佳选择,因为您可能希望能够将谓词传递给 API 的各种其他 DSL 方法。我不认为 IDE 集成、自动完成或文档有任何问题,只要你把它们都放在同一个工厂类中。具体来说,Eclipse 有很好的功能。您可以输入“收藏夹”,这导致所有方法都可以从自动完成下拉列表(这再次成为Javadocs的良好访问点)中随处可用。或者,您的用户可以从工厂中静态导入所有方法,只要他们需要它,结果相同。hascapableOfcom.example.Factory.*
  2. and, 应该是谓词类型上的方法(也可能是中心静态方法)。这导致了布尔组合的中缀表示法,许多开发人员认为这比JPA / CriteriaQuery所做的更直观:ornotnot

    public interface Predicate {
    
      // Infix notation (usually a lot more readable than the prefix-notation)
      Predicate and(Predicate... predicate);
      Predicate or(Predicate... predicate);
    
      // Postfix notation
      Predicate not();
    
      // Optionally, for convenience, add these methods:
      Predicate andNot(Predicate... predicate);
      Predicate orNot(Predicate... predicate);
    }
    
    public class Factory {
    
      // Prefix notation
      public static Predicate not(Predicate predicate);
    }
    
  3. 对于工会,您有几种选择。一些例子(你也可以组合):

    // Prefix notation
    public class Factory {
      public static Query union(Query... queries);
    }
    
    // Infix notation
    public interface Query {
      Query union(Query... queries);
    }
    
  4. 最后但并非最不重要的一点是,如果你想避免关键字,它是Java语言的一部分,而不是你的DSL的一部分,也要从工厂构造查询(DSL的入口点):new

    // Note here! This is your DSL entry point. Choose wisely whether you want
    // this to be a static or instance method.
    // - static: less verbose in client code
    // - instance: can inherit factory state, which is useful for configuration
    public class Factory {
    
      // Varargs implicitly means connecting predicates using Predicate.and()
      public static Query query(Predicate... predicates);
    
    }
    

通过这些示例,您可以按如下方式构造查询(您的示例):

高大的篮球运动员名叫[迈克尔·奥丹尼斯]

联盟

弯曲有光泽的银勺

Java 版本:

import static com.example.Factory.*;

union(
  query(is("tall"), 
        capableOf("basketball"), 
        name("michael").or(name("dennis"))
  ),
  query(color("silver"),
        a("spoon"),
        is("bent"),
        is("shiny")
  )
);

要获得进一步的灵感,请查看jOOQjRTF,它在Java中将RTF(“外部”DSL)建模为“内部”DSL方面也做得很好。


答案 2

使用静态导入时,必须使用伸缩模式才能使用不同的构造函数创建查询。伸缩构造函数模式可以工作,但是当有许多参数时,很难编写客户端代码,并且更难读取它。即使使用构建器的示例看起来也比使用静态导入更清晰。因此,在您的情况下,构建器似乎是更好的解决方案。


J.Bloch有一篇关于创建和销毁Java对象的好文章,这可能对你来说很有趣。


推荐