Don't avoid things just because they are considered evil - understand first why they are considered evil, and then decide how to avoid that evilness.
You are warned about these evil things because they are usually the easy, straightforward way to solve the problem - otherwise people wouldn't always attempt to do them and the warning would be redundant. The problem is that if you don't understand why method A is evil, you may end up using method B which is evil for the exact same reason as method A, but because method B is more awkward than A it was not very popular so nobody felt the need to warn you of it.
I can't find it, but I remember seeing avoiding singleton by making all the class' fields static
so that each new object of that class will use the same state(but there is no single instance
so it's not a singleton!)
Anyways, this is what you are doing with that master base class. Downcasting is not "evil" in your case, but that master base class suffers from the same problem that downcasting usually suffers from!
Why is downcasting considered evil?
Consider this:
public abstract class Base {
// bla bla bla
}
public class ImplA extends Base {
// bla bla bla
}
public class ImplB extends Base {
// bla bla bla
}
// somewhere else
void doit(Base base) {
if (base instanceof ImplA) {
ImplA implA = (ImplA)base;
// ImplA specific code
} else if (base instanceof ImplB) {
ImplB implB = (ImplB)base;
// ImplB specific code
}
}
Why is this bad? Because it doesn't handle ImplC
. But there is no ImplC
!!!
Well, there is no ImplC
now, but nothing stops me - or someone else - from writing it next year, making it extend Base
. And then they will create an instance of ImplC
, and that instance will be passed to doit
- which will probably handle it wrong. Because, while we don't know what ImplC
means and how doit
should handle it, if doit
needs special code for ImplA
and special code for ImplB
, we should assume it'll need special code for ImplC
. But you may not be able to add that code(because doit
may be part of a third party library), or you may simply not know you need to, because there is no compilation warning that doit
does not have special code to handle ImplC
. You'll realize eventually though, after a few hours/days of debugging trying to figure out why your program doesn't work...
This is why downcasting is frowned upon, and it's recommended to favor polymorphism and method overriding:
public abstract class Base {
// bla bla bla
public abstract void doit();
}
public class ImplA extends Base {
// bla bla bla
@Override
public void doit() {
// ImplC specific code
}
}
public class ImplB extends Base {
// bla bla bla
@Override
public void doit() {
// ImplB specific code
}
}
With this design, ImplC
can have it's own override of doit
with it's own specific code, and if you forget to write it you'll get a compilation error.
The problem with the master base class
I argue that your base class suffers from a similar problem - you need to modify the base class to add new functionality. It's not a worst case here, since you have access to the code and you won't forget to add the base methods because you need to use them. But still - you are avoiding downcasting by creating a construct that suffers from downcasting's general problem...
Downcasting is OK in this case
The problem with doit
was that it was meant to deal with any Base
, but in practice only handled the known types of Base
- ImplA
and ImplB
.
Your case is different. You are looping over a collection of A
, looking for, let's say, only instances of B
, downcasting them to B
and using them as B
. This loop is not meant to handle all instances of A
- it's only meant to handle the B
s in the collection. C
, D
, ... I
will have their own loops, probably elsewhere, that deal with them. And if you add a new subclass J
it'll also need new loop(s) - but writing these loops is a fundamental part of subclassing A
, not some random method somewhere that needs to be amended.
What if you need to handle multiple subclasses in the same loop?
If you find yourself writing something like this:
for (A a : theBigCollection) {
if (a instanceof B) {
B b = (B)a;
// B specific code
} else if (a instanceof G) {
G g = (G)a;
// G specific code
}
// Other subclasses are not handled here
}
You are repeating doit
's problem - if this loop needs to handle both A
and G
, how do we know it doesn't need to handle J
?
In this case, you should try to bundle this behavior with a mid subclass or with an interface - let's call it X
:
public interface X {
// bla bla bla
}
public class B extends A implements X {
}
public class G extends A implements X {
}
// the loop from before
for (A a : theBigCollection) {
if (a instanceof X) {
X x = (X)a;
// X specific code
}
}
Now if J
needs to, it can implement X
and get handled in this loop. This is probably what you need, since you mentioned a small set of subclasses implementing the same methods. You can have multiple such interfaces if you need to.
Epilogue
The problem with downcasting is the possibility of adding new subclasses that will go through the same code paths but won't have specialized code that handles them. So when considering whether or not you should use downcasting always think what will happen if someone adds a new subclass.
I believe this is a good place to use the wider interpretation of the Zero-One-Infinity Rule. In any code "unit", you should handle zero, one or infinite(==all possible) subclasses of A
:
- zero: You don't use
A
in that code. Not interesting
- one: One subclass, or one interface that you try to cast to. But not "one" as in "I have one apple" - it should be one as in "God is one". It should be conceptually impossible to add alternative downcasts, no matter what other subclasses will be added there you are still allowed to change the one type to some interface - as long as it remains one.
- infinity: You deal with all possible subclasses of
A
- which means you don't downcast and use A
itself.
Note that there may be multiple levels - in my last example I was first downcasting to X
(one), and then writing code for X
which will work on all possible subclasses of X
(infinity).