As always I'll illustrate my point with an example. We'll start with the standard (non-structural) approach.
(Side note: In the very first lecture at the Programming-Languages class I took, my teacher, Prof. Ron Pinter, made the point that a programming language is an excellent device for formally expressing ideas: "Instead of battling a natural language hopelessly trying to get the precise meaning, just write the thing in a PL and your intention will be clear". I think this is a very good advice)
Suppose someone has finally written the huge algorithm that realizes the overwhelmingly complicated logic of the bridge-keeper from Monty Python and the Holly Grail. The algorithm is encapsulated within the Bridgekeeper class whose protocol relies on the Knight interface.
public interface Knight {
public String answerMe(String question);
}
class Birdgekeeper {
public boolean canCross(Knight k) { ... }
}
My friend Graham developed an AbstractKnight class which provides some services common to all knights:
public abstract class AbstractKnight
implements Knight {
private final String name;
protected AbstractKnight(String name_) {
name = name_;
}
protected abstract String nonNameQuestion(String q);
public String answerMe(String question) {
if("What is your name?".equals(question))
return name;
String answer = nonNameQuestion(question);
if(answer == null)
answer = "I don't know that";
return answer;
}
}
If I want to develop a concrete subclass of AbstractKnight (Lancelot or Robin come to mind), Graham needs to send me two jars (or two source file packages):
- Graham.jar - Providing the definition of the AbstractKnight class
- Bridgekeeper.jar - Providing the definition of the Knight interface
Yes. Without Bridgekeeper.jar I will not be able to compile my Lancelot class, despite the fact that Lancelot is only indirectly coupled with Knight. When you compile a class the compiler requires all its ancestors to be present.
Graham is therefore forced to distribute Bridgekeeper.jar to allow other programmers to subclass his AbstractKnight class. This often leads to proliferation of download-able packages (with/without dependencies) which is a constant headache for open source developers. Of course, if you choose to download the full package (the one that does include the dependencies) you are then risking having several different version of the same library on your machine, which - again - leads to headaches.
With structural subtyping things are much simpler:
// library: Bridgekeeper.jar
public struct Knight {
public String answerMe(String question);
}
... // everything else remains the same
// library: Graham.jar
public abstract class AbstractKnight {
// "implements Knight" removed
... // everything else remains the same
}
With this approach, AbstractKnight has no compile-time dependency on Knight. Thus, you can compile your Lancelot class even if Bridgekeeper.jar is not present on your classpath:
public class Lancelot extends AbstractKnight {
public Lancelot() {
super("Sir Lancelot of Camelot");
}
protected String nonNameQuestion(String q) {
if("What is your quest?".equals(q))
return "To seek the Holy Grail";
if("What is your favorite color?".equals(q))
return "Blue";
return null;
}
}
Update 1: OK. I couldn't resist the temptation. Here it is, in its full glory, scene 35:
0 comments :: Structural Subtyping == Reduced Compile-Time Coupling
Post a Comment