Thursday, November 25, 2004

Why Java needs delegation

The problem

A problem especially exacerbated in Java by inversion of control (IoC) is composition or delegation v. inheritance. In C++, this is a non-issue given features such as multiple inheritance, private inheritance and using declarations. However, Java has no such help. Just look at this code:

public interface Commuter {
    public void walkToWork();
    public void driveToWork();
    public void flyToWork();
}

And yet another interface:

public interface Parent {
    public void produceChildren();
    public void feedChildren();
    public void disciplineChildren();
    public void teachChildren();
}

And finally a concrete class:

public class Dad
        implements Commuter, Parent {
    private Commuter commuter;
    private Parent parent;

    public Dad(final Commuter commuter, final Parent parent) {
        this.commuter = commuter;
        this.parent = parent;
    }

    public void walkToWork() {
        commuter.walkToWork();
    }

    public void driveToWork() {
        commuter.driveToWork();
    }

    public void flyToWork() {
        commuter.flyToWork();
    }

    public void produceChildren() {
        parent.produceChildren();

Hey, wait! You didn't finish! you cry. You are right; I got bored with all that redundant typing. This is the problem with delegation in Java — the language support sucks and it's all hand-made. What Java needs is proper delegation support.

In C++ it looks like this:

class Dad
  : public HoustonCommuter, public GoodParent
{
};

That's it! (Given suitable implementations of HoustonCommuter and GoodParent.) Even better, just like in Java you can change the implementation:

template<typename A, typename B>
class CommutingParent
  : public A, public B
{
};

typedef CommutingParent<HoustonCommuter, GoodParent> Dad;

If anything, it's now too amazing as you can construct a Dad from the wrong base classes, although latent typing keeps this problem theoretical. (However, note the C++ FAQ on private inheritance as a counter.)

What's to be done?

The solution

The solution is to change Java, of course. :-) I don't suggest C#'s delegate approach, and introducing new keywords is bad policy. What I suggest is both simpler and more expressive, and also more flexible. Let's turn back to the Commuter/Parent example. Try your mind on this:

class Dad implements Commuter, Parent {
    public Dad(final Commuter commuter, final Parent parent) {
        this = commuter;
        this = parent;
    }
}

What's going on here? I'm teaching the Java compiler to take meaning from an otherwise illegal statement: assignment to this. I wave my compiler-writer wand and declare that when a class implements an interface, assignment to this implements that interface with the object assigned (the RHS) provided that the RHS implements the interface.

Internally, I'd teach the compiler for this example to add a hidden Commuter __commuter instance member to Dad, assign to that member, and add the forwarding methods to Dad automatically. I'd expect reflection to work as expected and make all this clear, just as if I'd had coded it all by hand. No magic secrets, please.

More details

If Dad directly implements any of the methods in Commuter or Parent, those let the compiler off the hook and it generates no silent methods. That is:

public class Dad implements Commuter, Parent {
    public Dad(final Commuter commuter, final Parent parent) {
        this = commuter;
        this = parent;
    }

    public void teachChildren() {
        teachSinging();
    }

    private void teachSinging() {
        System.out.println("Mir ist so wunderbar!");
    }
}

This implementation of Dad should provide silent methods for everything except void teachChildren(). Note that this implies you can implement portions of an interface as well:

public interface CommutingParent extends Commuting, Parent {
}

public class Dad implements CommutingParent {
    public Dad(final Commuter commuter) {
        this = commuter;
    }
}

Now Dad will not compile until it implements the methods in Parent. However this leads to an interesting question: what about ambiguities?

public class Dad implements CommutingParent {
    public Dad(final Commuter commuter, final CommutingParent commutingParent) {
        this = commuter;
        this = commutingParent
    }
}

Dad will not compile with the compiler complaining about an ambiguity: which delegate implements Commuter, commuter or commutingParent? The simple approach is best to fix this error:

public class Dad implements CommutingParent {
    public Dad(final Commuter commuter, final CommutingParent commutingParent) {
        this = commuter;
        this = (Parent) commutingParent;
    }
}

Here the (Parent) cast extracts the just the Parent implementation from commutingParent and ignores the Commuter implementation.

A final problem

Back to the earlier example:

public class Dad implements Commuter, Parent {
    public Dad(final Commuter commuter, final Parent parent) {
        this = commuter;
        this = parent;
    }

    public void teachChildren() {
        teachSinging();
    }

    private void teachSinging() {
        System.out.println("Mir ist so wunderbar!");
    }
}

Notice anything funny? Contrast with this:

public class Dad extends HoustonCommuter, GoodParent {
    public void teachChildren() {
        super.teachSinging();
        teachSinging();
    }

    private void teachSinging() {
        System.out.println("Mir ist so wunderbar!");
    }
}

Ok, we call super.teachChildren() before extending the behavior of void teachChildren() so that we add to instead of replace existing behavior. How can I accomplish that now with delegation? Simple, write the exact same code! Since I'm giving meaning to assignment to this, I feel free to extend the meaning of calls through super. The compiler will take this as a request to inline the code for the silent delegation method. It will still not generate the silent method as we need reflection to find the our own code, not the silent method.

The only remaining problem is (ok, so the preceding problem was the penultimate one, not the final one):

public class BeetleDriver implements Driver {
    public void driveToWork() {
        System.out.println("Baby you can drive my car!");
    }
}

And:

public class Dad extends BeetleDriver implements CommutingParent {
    public Dad(final CommutingParent commutingParent) {
        this = commutingParent;
    }
}

Who implements void driveToWork()? It's another ambiguity error and the class will not compile. Back to explicit delegation:

public class Dad extends BeetleDriver implements CommutingParent {
    public Dad(final CommutingParent commutingParent) {
        this = commutingParent;
    }

    public void driveToWork() {
        super.driveToWork();
    }
}

This is just like the solution above for super.teachChildren().

Post a Comment