When laying out a class hierarchy, I often find myself frustrated at the gap between being able to encapsulate functionality while also sharing code. Part of the problem, of course, is lack of multiple inheritance, but interfaces help somewhat. The inability to define protected methods on interfaces seems to me to be the bigger issue.
The standard solution seems to be to have a public interface that is implemented by a protected abstract base class. The problem is when we have the following
public interface Foo {
public String getName();
}
abstract protected BaseFoo implements Foo {
abstract protected int getId();
private String name;
protected BaseFoo(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
public class ConcreteFoo extends BaseFoo {
public ConcreteFoo (String name) {
super(name);
}
@Override
protected int getId() {
return 4; // chosen by fair dice roll.
// guaranteed to be random.
}
}
// in the foo package with the classes above
public class FooCollection {
private static Map<Integer, Foo> foos = new HashMap();
public static void add(Foo foo) {
synchronized(foos) {
foos.put(foo.getId(), foo); // can't call foo.getId()
}
}
}
// client code, not in the foo package
FooCollection.add(new ConcreteFoo("hello world"));
That is, we return one of our nicely-encapsulated objects to caller, but then any method which gets that object back needs to be able to rely on some internal functionality. That internal functionality cannot be part of the interface (that would break encapsulation), but to make it part of an abstract base class requires us to use casting.
We cannot make Foo an abstract class because other interfaces need to extend it to add optional, orthogonal functionality to a more complex hierarchy than is display here.
What are the standard approaches to this problem? Do you add getId to the Foo interface, even though clients shouldn’t use it? Do you perform an unsafe cast to BaseFoo in FooCollection.add? If you check before casting, what do you do when the types don’t match, even though they always should for all intents and purposes?
Any information you have on best practices in this sort of situation would be very helpful.
Edit: In case it’s not clear, this example is intentionally oversimplified. The key point is that sometimes you return an “interface view” of an object. When that “interface view” is passed back in to a package-specific class, the method it is passed to will likely need to use internal functionality in its implementation. How does one manage that mismatch between internal and public functionality?
Okay, here’s a couple of points:
Contrary to popular opinion, inheritance really isn’t about sharing code. What you create in an inheritance hierarchy is an organization of things that share some common set of abstract behaviors; it just works out sometimes to have the effect of reusing some code.
The fashion has changed quite a bit in the last few years, so that deep and complicated inheritance hierarchies are no longer considered good form. In general in Java. you should
If you really want the effect of multiple inheritance, build implementation classes for your interfaces, and aggregate them.
In particular, by defining your classes with interfaces and implementation classes, you make building tests much easier; if your interface is separate, it’s almost trivial to build a mock for that interface.