17

I am creating a custom query class, and i am unsure about the most elegant way to code it.

The goals are:

  • Easy to use
  • Extensibility
  • Flexible so that complex queries can be formulated

Approaches

Currently i can think of two alternatives.

1. Builder pattern

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

The methods is(), capableOf() and name() return a self-reference to the Query object. build() will return a Result object.

2. Static Imports

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

The methods is(), capableOf() and name() are static imports and return Condition objects. The Query constructor takes an arbitrary number of conditions and returns the result.

And/Or/Not queries

More complex queries like the following are complicated to formulate:

tall basketball player named [michael OR dennis]

UNION

silver spoon which is bent and shiny

Builder pattern:

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();

This is difficult to write and read. Also, i do not like the multiple use of new.

Static imports:

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

Looks better to me, but i do not really like the use of static imports. They are difficult in terms of ide integration, auto-completion and documentation.

Sum up

I am looking for an effective solution, therefore i am open to suggestions of any kind. I am not limited to the two alternatives i presented, if there are other possibilities i'd be happy if you tell me. Please inform me if you need further information.

Alp
  • 29,274
  • 27
  • 120
  • 198
  • 2
    if its a database query your code look svery similar to this library ...http://www.jooq.org/. And you have considered using an ORM like hibernate – NimChimpsky Mar 30 '12 at 12:00
  • No, it's not related to databases. It's about queries for elements on any web page. – Alp Mar 30 '12 at 12:06
  • webpages!? http://jquery.com/ would be my first port of call then – NimChimpsky Mar 30 '12 at 12:10
  • jOOQ looks like a combination of both alternatives, static imports in a builder pattern. interesting – Alp Mar 30 '12 at 12:11
  • @NimChimpsky: Yes, i use jQuery in the core, but this is generally a Java wrapper with more functionality. – Alp Mar 30 '12 at 12:11
  • jquery in the core ? java wrapper for jquery ? I have no idea what your building. But yeah builder pattern (for complex objects) and method chaining both nice and easy to use in an api. – NimChimpsky Mar 30 '12 at 12:15
  • @NimChimpsky: This is the project i am talking about: https://github.com/alp82/abmash – Alp Mar 30 '12 at 12:43
  • oh right this might be helpful if you haven't seen it already http://htmlunit.sourceforge.net/, interesting concept btw – NimChimpsky Mar 30 '12 at 13:06
  • @NimChimpsky: Though i'd like to use HtmlUnit, it's not an option because it does not support extensive AJAX, Canvas, etc. – Alp Mar 30 '12 at 13:10
  • This question has no "correct" answer, it would be better suited on http://programmers.stackexchange.com/ – Jesse Webb Mar 30 '12 at 18:13

2 Answers2

31

You are about to implement a domain specific language (DSL) in Java. Some would refer to your DSL as being an "internal" DSL, because you want to use standard Java constructs for it as opposed to "external" DSLs, which are much more powerful (SQL, XML, any type of protocol), but have to be constructed primitively using string concatenation.

Our company maintains jOOQ, which models SQL as "internal" DSL in Java (this was also mentioned in one of the comments). My recommendation for you is that you follow these steps:

  1. Become aware of what your language should look like. Don't think in terms of Java ("internal" DSL) right away. Think in terms of your very own language ("external" DSL). The fact that you will implement it in Java should not be important at that point. Maybe you'll even implement it in XML, or you'll write your own parser/compiler for it. Thinking about your language specification first, before implementing it in Java will make your DSL more expressive, more intuitive, and more extensible.
  2. Once you've settled for the general syntax and semantics of your language, try drawing a BNF notation of your language. You don't have to be overly precise at the beginning, but this will give it some formal aspects. Railroad diagrams is a very nice tool for that. You will become aware of which combinations are possible and which ones aren't. Also, it is a good way to create an overall language documentation, because single-method Javadocs won't be much help to your newbie users.
  3. When you have a formal syntax, follow the rules that we have mentioned in our blog here: http://blog.jooq.org/2012/01/05/the-java-fluent-api-designer-crash-course. These rules have proven very useful when designing the jOOQ API, which has been reported by our users to be very intuitive (if they already know SQL, that is).

My personal recommendation for you is this:

  1. is, has, capableOf, etc are predicate factory methods. Static methods are your best choice in Java, because you will probably want to be able to pass predicates to various other DSL methods of your API. I don't see any problem with IDE integration, auto-completion, or documentation, as long as you put them all in the same factory class. Specifically Eclipse has nice features for that. You can put com.example.Factory.* to your "favourites", which leads to all methods being available everywhere from the auto-completion dropdown (which is again a good access-point for Javadocs). Alternatively, your user can just static-import all methods from the Factory wherever they need it, which has the same result.
  2. and, or, not should be methods on the predicate type (not may also be a central static method). This leads to an infix notation for boolean combinations, which is considered more intutitve by many developers, than what JPA/CriteriaQuery did:

    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. For unions, you have several options. Some examples (which you can also combine):

    // Prefix notation
    public class Factory {
      public static Query union(Query... queries);
    }
    
    // Infix notation
    public interface Query {
      Query union(Query... queries);
    }
    
  4. Last but not least, if you want to avoid the new keyword, which is part of the Java language, not of your DSL, do also construct queries (the entry points of your DSL) from a Factory:

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

With these examples, you can construct queries as such (your example):

tall basketball player named [michael OR dennis]

UNION

silver spoon which is bent and shiny

Java version:

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")
  )
);

For further inspiration, have a look at jOOQ, or also at jRTF, which also does an excellent job at modelling RTF ("external" DSL) in Java as an "internal" DSL

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Marvellous answer, thank you very much. I will follow your advices and hopefully come back with some results soon. – Alp Mar 31 '12 at 17:36
  • 1
    @Alp: Sure. Feel free to discuss more details on the [jooq user group](https://groups.google.com/forum/?fromgroups#!forum/jooq-user) or on the [above blog post](http://lukaseder.wordpress.com/2012/01/05/the-java-fluent-api-designer-crash-course). Maybe I can learn something, too, from your use-cases – Lukas Eder Mar 31 '12 at 23:39
  • Turns out that building JQuery commands with arbitrary nested AND/OR/NOT predicates is a real brain twister. – Alp Apr 05 '12 at 10:05
  • @Alp: What are the problems that you're facing? – Lukas Eder Apr 05 '12 at 11:26
  • I need some time to formulate a problem description. It's hard to model a generic approach that works with any combination of boolean predicates, because JQuery is not designed for complex queries. – Alp Apr 05 '12 at 17:41
1

With static imports you have to use telescopic pattern for ability to create Query with different constructors. The telescoping constructor pattern works, but it is hard to write client code when there are many parameters, and harder still to read it. Even your examples with builder looks more clear than with static imports. So in your case builder seems to be better solution.


There is a good article by J.Bloch about Creating and Destroying Java Objects which could be interesting for you.

amukhachov
  • 5,822
  • 1
  • 41
  • 60
  • Thanks for your thoughts and the link. The telescoping pattern is indeed not an option due to a very large amount of condition types. Do you really think that my examples with the builder looks more clear than with static imports? I'd be happy if you elaborate more on that. – Alp Mar 30 '12 at 13:19