When I find something interesting and new, I post it here - that's mostly programming, of course, not everything.

Thursday, December 11, 2008

Java Solutions: inheriting chain calls

Recently chain builder calls in Java have been gaining popularity, look, e.g. at this.

The idea is that you have a builder that has small configuration methods and in the end builds the right stuff. StringBuffer works like this:

 return new StringBuffer().append("Now is ").append(new Date()).append(", and ticking...").toString();


Or, more typically, something like this code:

new TestBuilder().limitTime(1000).forPackage(this.getClass().getPackage()).skipFlaky().build().run();


This way we, first, annotate our parameters, and second, bypass the boring law that demands writing each statement on a separate line.
And of course it makes the process more flexible.

Actually, the fact that it is a builder is unimportant. We can just chain calls if every method that in the previous life was returning void would start returning this.

But there's a problem here... what if our builder is not the ultimate version, but a subclass of a superbuilder? Look at an example below:


public class ChainCalls {
public static class A {
A do1(String s) {
System.out.println("A.1 " + s);
return this;
}

A do2(String s) {
System.out.println("A.2 " + s);
return this;
}
}

public static class B extends A {
B do0(String s) {
System.out.println("B.0 " + s);
return this;
}

B do1(String s) {
System.out.println("B.1 " + s);
return this;
}

B do3(String s) {
System.out.println("B.3 " + s);
return this;
}
}

public static void main(String[] args) {
((B)(new B().do0("zero").do1("one").do2("two"))).do3("three");
}
}


See how we have to cast the result of do2()? That's because the method of class A has no knowledge that it actually works in the context of class B.

How can we deal with this? One cheap solution is to duplicate all the methods in B and call the delegate, that is, super.do1() etc. Feasible, but not very nice, not very scalable, and not very Java5-ish.

Because we have generics, and we have a so-called covariant inheritance, so that - because we can!

An anonymous contributor in livejournal.com has suggested the following solution:

public class ChainCallsCovariant {
public static abstract class SuperA<T> {
abstract T self();

T do1(String s) {
System.out.println("A.1 " + s);
return self();
}

T do2(String s) {
System.out.println("A.2 " + s);
return self();
}
}

public static class A extends SuperA<A> {
A self() { return this; }
}

public static abstract class SuperB<T> extends SuperA<T> {
T do0(String s) {
System.out.println("B.0 " + s);
return self();
}

T do1(String s) {
System.out.println("B.1 " + s);
return self();
}

T do3(String s) {
System.out.println("B.3 " + s);
return self();
}
}

public static class B extends SuperB<B> {
B self() { return this; }
}

public static void main(String[] args) {
new B().do0("zero").do1("one").do2("two").do3("three");
}
}


The main trick here is to encapsulate "return this", and make it generic, so that we always, in the eyes of the compiler, return the instance of the right class.

P.S. Here I've posted a better solution.

4 comments:

Sergey Malgin said...

hey, first link is broken

Vlad Patryshev said...

Thanks; link fixed.

Ran said...

This is really clever, but then instances of B are not instances of A; that is, there's no IS-A relationship between B and A. (In more formal terms, you've gained implementation inheritance but lost type inheritance.) This means that a client method designed to accept an instance of A cannot be used with an instance of B.

(I'm assuming that we consider this to be a problem. If we have no desire for instances of B to be instances of A, then I guess that's fine, but I think it's a pretty ugly restriction.)

Now, the client method can be written to accept an instance of SuperA<?> instead of A, and everything will work fine. The only problem is, it won't be able to use the method chaining you've worked so hard for, because every method's return type will be the unrestricted wildcard!

You can solve this partly by declaring your classes more restrictively — instead of class SuperA<T>, it could be class SuperA<T extends SuperA<? extends T>> (and so on) — Java's type-capture rules are smart enough to apply recursively, so that you could then write sa.do1("").do2("").do1("") where sa is an expression of type SuperA<?> — but you're still complicating your type hierarchy, and requiring clients to be aware of what's basically an implementation detail. Any class that wants to create a new instance of A will need to be aware of A so it can call its constructor, and any class that wants to perform operations on instances of A will want to be aware of SuperA so it can still work on instances of B (as well as other, as-yet-unknown subtypes).

That, you can partly solve by changing SuperA, A, SuperB, and B to A, A.Concrete, B, and B.Concrete (respectively), making the concrete classes be private static member classes of their supertypes, and having clients create instances by calling a static A.newInstance() (which would be declared as type A<?>) instead of calling a constructor directly (and likewise for subclasses).

And you end up with a heck of a lot of dense boilerplate code, plus you've forced your clients to write A<?> everywhere (providing a wildcard type argument representing an implementation detail that from their perspective serves no purpose whatsoever), which works fine but is kind of silly.

Also, remember when I said that Java's type-capture rules are smart enough for that? Well, they are, but there might be down-sides. Eclipse 3.3.2 (which we use at my workpace) is smart enough to apply these rules to distinguish legal code from illegal code, but when you type a.do1(""). and it gives you a list of applicable methods, some sort of internal exception seems to get thrown (and caught) internally: it doesn't even list all the methods belonging to Object. (But I don't know how other versions of Eclipse behave, much less how other IDEs do.)

All told, I'm not sure how often it's worth it to take this sort of approach, rather just overriding methods and delegating to the superclass. (Keeping in mind that this whole thing is a patch over one small, specific problem: a method that doesn't exist in class A can't be chained to a method that's not overridden in class B.)

Vlad Patryshev said...

Ran, not that I fully agree with you or fully understand all the arguments, but, well, you are right.

I've come up lately with a better solution, and will post it soon (probably in the end of June).

In short, it amounts to defining self() in A:

T self() {
return (T) this;
}

Followers

Subscribe To My Podcast

whos.amung.us