Agreement with developers
Chain programming style is a common design in Java library API design.
There are two motivations for the chained API design:
Create a new object without modifying the current object. Commonly seen with immutable objects such as
String
String value = " Hello world "
.trim() // α
.toUpperCase() // β
.replace(' ', '-'); // γIn the example, the three method calls at
α
,β
andγ
do not modify the current string, but create a new string.For this usage, the chained API itself is the design goal. The user must pay attention to the return value of each call, discarding the return value makes the method call meaningless.
Modifies the current object and returns the current object itself. Common with mutable objects such as
StringBuilder
StringBuilder value = new StringBuilder()
.append("Hello ") // α
.append(' ') // β
.append(System.getProperty("user")); // γIn the example, the three method calls at
α
,β
andγ
all return the current object itself, never create a new object, and the code operates on the sameStringBuilder
object from beginning to end.For this usage, the chained API is not a design goal in itself, it's just to simplify the code. For its return value, it doesn't matter whether the user chooses to use or discard it.
:::note This design motivation is to address a small flaw in Java's expressiveness, a trick that is not needed in more modern languages. :::
The chain API with the same style corresponds to two completely different design motives, and correspond to completely different understanding and usage.
Except for the popular base types such as String
and StringBuilder
, for most frameworks, what kind of motivation a chained API corresponds to often becomes part of the user's learning cost. Even, sometimes it becomes a source of confusion. For example JPA Criteria's Predicate.not method, from the documentation, its design motivation should be 2
, but cannot prevent some JPA venders implement it as 1
.
Jimmer does not want to make identifying the design motivation of a chained API part of the learning cost for users, and defines two annotations.
@org.babyfish.jimmer.lang.NewChain
@org.babyfish.jimmer.lang.OldChain
The RetentionPolicy of these two annotations is SOURCE, which does not affect the bytecode and reflection behavior, only as part of the function signature.
- @NewChain corresponds to motivation 1, indicating that the current chain API always creates new objects without modifying the current object.
- @OldChian corresponds to motivation 2, saying that the current chain API always modifies and returns the current object, without creating a new one.
Take the two interfaces of the jimmer-sql subproject as an example
- Java
- Kotlin
package org.babyfish.jimmer.sql.ast.query;
import org.babyfish.jimmer.lang.OldChain;
public interface MutableRootQuery<T extends Table<?>>
extends MutableQuery, RootSelectable<T> {
@OldChain
MutableRootQuery<T> where(
Predicate... predicates
);
@OldChain
@Override
default MutableRootQuery<T> orderBy(
Expression<?> expression
);
@OldChain
default MutableRootQuery<T> orderBy(
Expression<?> expression,
OrderMode orderMode
);
@OldChain
MutableRootQuery<T> orderBy(
Expression<?> expression,
OrderMode orderMode,
NullOrderMode nullOrderMode
);
@OldChain
MutableRootQuery<T> groupBy(
Expression<?>... expressions
);
@OldChain
MutableRootQuery<T> having(
Predicate... predicates
);
}
Kotlin doesn't need to use chained API design tricks, so kotlin API won't use @OldChain
- Java
- Kotlin
package org.babyfish.jimmer.sql.ast.query;
import org.babyfish.jimmer.lang.NewChain;
import java.util.function.BiFunction;
public interface ConfigurableRootQuery<T extends Table<?>, R>
extends RootQuery<R> {
@NewChain
<X> ConfigurableRootQuery<T, X> reselect(
BiFunction<
MutableRootQuery<T>,
T,
ConfigurableRootQuery<T, X>
> block
);
@NewChain
ConfigurableRootQuery<T, R> distinct();
@NewChain
ConfigurableRootQuery<T, R> limit(int limit, int offset);
@NewChain
ConfigurableRootQuery<T, R> withoutSortingAndPaging();
@NewChain
ConfigurableRootQuery<T, R> forUpdate();
}
package org.babyfish.jimmer.sql.kt.ast.query
import org.babyfish.jimmer.lang.NewChain
interface KConfigurableRootQuery<E: Any, R> : KRootQuery<R> {
@NewChain
fun <X> reselect(
block: KMutableRootQuery<E>.() -> KConfigurableRootQuery<E, X>
): KConfigurableRootQuery<E, X>
@NewChain
fun distinct(): KConfigurableRootQuery<E, R>
@NewChain
fun limit(limit: Int, offset: Int): KConfigurableRootQuery<E, R>
@NewChain
fun withoutSortingAndPaging(): KConfigurableRootQuery<E, R>
@NewChain
fun forUpdate(): KConfigurableRootQuery<E, R>
}
Jimmer's chained API will frequently use these two annotations. After understanding this convention, there is no learning cost in this regard.