I am creating a custom query class, and i am unsure about the most elegant way to code it.
The goals are:
Currently i can think of two alternatives.
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.
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.
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.
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.
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:
My personal recommendation for you is this:
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.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);
}
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);
}
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With