I have a large number of classes with an abstract base class (A) that contains behaviour that must be supported by all the sub classes say [B..I].
In my code, I end up with a collection of objects that is created based on input from an external system. These objects belong to subclasses described above and they go through various operations in my code.
All of these operations require some core and common behaviour so I use the abstract base class and I can call methods on a collection of objects using only the base type ( A.DoTheThing() ).
The problem is, only a small subset of these types must support a common set of functionality. If I add the methods to the base class with a default implementation that does nothing or returns null then I'm going towards a God class. Only a few subtypes would override these methods but I can keep using the base type to process the collection. The large group of subclasses would do nothing in response to method calls based on the default empty implementation in the base class. The ones that override the default behaviour would do what they need to do.
If I don't want to put behaviour where it mostly does not belong, I'd have to define an interface (X) and implement it for the small subset of subtypes that'll be included in the collection. However, I now have a collection based on the type A and at some point after using methods from A, I need to perform operations on the subset of objects that implement X. The only option I'm left with is to filter the collection based on instanceof(X) and call relevant methods.
Which one is the lesser evil and do I have another choice here?
Best Answer
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 singleinstance
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:
Why is this bad? Because it doesn't handle
ImplC
. But there is noImplC
!!!Well, there is no
ImplC
now, but nothing stops me - or someone else - from writing it next year, making itextend Base
. And then they will create an instance ofImplC
, and that instance will be passed todoit
- which will probably handle it wrong. Because, while we don't know whatImplC
means and howdoit
should handle it, ifdoit
needs special code forImplA
and special code forImplB
, we should assume it'll need special code forImplC
. But you may not be able to add that code(becausedoit
may be part of a third party library), or you may simply not know you need to, because there is no compilation warning thatdoit
does not have special code to handleImplC
. 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:
With this design,
ImplC
can have it's own override ofdoit
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 anyBase
, but in practice only handled the known types ofBase
-ImplA
andImplB
.Your case is different. You are looping over a collection of
A
, looking for, let's say, only instances ofB
, downcasting them toB
and using them asB
. This loop is not meant to handle all instances ofA
- it's only meant to handle theB
s in the collection.C
,D
, ...I
will have their own loops, probably elsewhere, that deal with them. And if you add a new subclassJ
it'll also need new loop(s) - but writing these loops is a fundamental part of subclassingA
, 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:
You are repeating
doit
's problem - if this loop needs to handle bothA
andG
, how do we know it doesn't need to handleJ
?In this case, you should try to bundle this behavior with a mid subclass or with an interface - let's call it
X
:Now if
J
needs to, it can implementX
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
:A
in that code. Not interestingA
- which means you don't downcast and useA
itself.Note that there may be multiple levels - in my last example I was first downcasting to
X
(one), and then writing code forX
which will work on all possible subclasses ofX
(infinity).