Is narrowing of overriden method arguments types a Liskov Substitution Principle violation?

StackOverflow https://stackoverflow.com/questions/10197594

  •  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?

Was it helpful?

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top