Is narrowing of overriden method arguments types a Liskov Substitution Principle violation?
-
01-06-2021 - |
Question
I have this code:
abstract class Entity
{
// blah-blah-blah
}
abstract class BaseCollection
{
public void add(Entity entity);
}
And I derive from the Entity and BaseCollection classes:
class User extends Entity
{
}
class UserCollection extends BaseCollection
{
public void add(User user) { // blah-blah-blah }
}
Is this an example of Liskov Substitution Principle violation? If it is, how can I solve the issue?
Solution
As User
is a subtype of Entity
it is perfectly reasonable to add such objects to BaseCollection
(via UserCollection
) -- each user is an Entity
Passing UserCollection
where BaseCollection
is expected, will not work on the other hand: you are expecrted to be able to add an Entity
, but you need a User
-- or in other words: when you get an element out of the UserCollection
, you might get an Entity
after this, where you expect a User
.
OTHER TIPS
It is a violation of the Liskov Substitution Principle as other implementations of Entity could not be added to UserCollection. A user with a reference to a BaseCollection will not expect implemenations that are UserCollections to explode if they provide an Entity other than a User.
I'm assuming that UserCollection.add is replacing BaseCollection.add as you explicitly mentioned narrowing and didn't specify a language.
Method parameters should be contravariant, not covariant if you are following the Liskov Substitution Principle. http://en.wikipedia.org/wiki/Liskov_substitution_principle.
If the contract for BaseCollection
specifies that its add
method may legitimately be passed any object derived from Entity
, then the inherited add
method of UserCollection
should do likewise, and failure to do so would be a violation of the LSP. Having UserCollection
contain an overload (not override) of add
which only accepts objects of type User
would not violate the LSP if the original add
method could be used with arbitrary objects derived from Entity
, though overloading would likely not be particularly appropriate.
If, instead of add
, the method in question had been something like setItem(int index, Entity value)
in the base and setItem(int index, User value)
in the derived class, and if the contract specified that it was only guaranteed to work with objects which had been read out of the same collection, then provided that reading the UserCollection
would never yield anything other than an instance of User
, the setItem
method could legitimately reject all objects that weren't instances of User
without violating the LSP. If the setItem
method is going to reject everything that isn't an instance of user
, then having an overload which accepts only user
may be useful and appropriate; even though the inherited setItem
method would need to verify that value
identified an instance of User
, an overload which accepted an argument of that type would not. The biggest caveat when adding such an overload is one should avoid having two unsealed virtual methods that do the same thing; if one is going to add an overload, one should probably override and seal the base-class method so that it converts a passed-in argument to type User
and then chains to the overloaded version of the method.
Note that arrays subscribe to the latter form of contract and inheritance; a variable of type Animal[]
may hold a reference to a Cat[]
; an attempt to store an arbitrary Animal
into an Animal[]
which identifies a Cat[]
could fail, but any Animal
read out of an Animal[]
is guaranteed to "fit" in that same array; this makes it possible for code to sort or permute the elements in an array of arbitrary reference type without having to know the type of the array in question.