Many aspects of programming involve tradeoff, and SOLID principles are among them. If there are some kinds of action which can be done in the same way to nearly all derivatives of a class or implementations of an interface, and aren't really part of the main purpose of the interface, but a few particular derivatives or implementations may have a better way of doing them, the "Interface Segregation Principle" would suggest that such actions not be included in the common interface(*). In such cases, it may be helpful for code which receives a reference to something of non-specific type to check whether the actual object has certain "special" features and use them if so. For example, code which receives an IEnumerable<Animal>
and wants to know how many items it contains may check whether it implements ICollection<Animal>
or the non-generic ICollection
[note that List<Cat>
implements the latter but not the former] and--if so--downcast and use the Count
method]. There is nothing wrong with downcasting in such cases since the method doesn't require that passed-in instances implement those interfaces--it merely works better when they do.
(*) IMHO, IEnumerable
should have included a method to describe properties of the sequence, such as whether the count was known, whether it would forevermore contain the same items, etc. but it doesn't.
Another use of downcasting occurs in cases where one will have a collection of groups of objects, and know that the particular object instances within each group are "compatible" with each other, even though objects in one group may not be compatible with objects in another. For example, the MaleCat.MateWith()
method may only accept an instance of FemaleCat
, and FemaleKangaroo.MateWith()
with may only accept an instance of MaleKangaroo()
, but the most practical way for Noah to have a collection of mating pairs of animals would be for each type of animal to have a MateWith()
method that accepts an Animal
and downcasts to the proper type (and probably also have a CanMateWith()
property). If a MatingPair
is constructed containing a FemaleHamster
and MaleWolf
, an attempt to invoke the Breed()
method on that pair would fail at runtime, but if code avoids construction of incompatible mating pairs, such failures should never occur. Note that generics can substantially reduce the need for this sort of downcasting, but not entirely eliminate it.
In determining whether downcasting violates the LSP, the $50,000 question is whether a method will uphold its contract for anything that might get passed in. If the MateWith()
method's contract specifies that it is only guaranteed to behave usefully on particular instances of Animal
for which CanMateWith()
has returned true, the fact that it will fail when given some subtypes of Animal
would not be an LSP violation. In general, it's useful to have methods reject at compile time objects whose type isn't guaranteed to be usable, but in some cases code may have knowledge about relationships among the types of certain object instances which cannot be expressed syntactically [e.g. the fact that a MatingPair
will hold two Animal
instances that can be successfully bred]. While downcasting is often a code smell, there's nothing wrong with it when it's used in ways consistent with an object's contract.