我有这个代码:
abstract class Entity
{
// blah-blah-blah
}
abstract class BaseCollection
{
public void add(Entity entity);
}
我从 Entity 和 BaseCollection 类派生:
class User extends Entity
{
}
class UserCollection extends BaseCollection
{
public void add(User user) { // blah-blah-blah }
}
这是违反利斯科夫替代原则的一个例子吗?如果是,我该如何解决问题?
User
是Entity
的子类型,因此将此类对象添加到BaseCollection
(通过UserCollection
)是完全合理的 - 每个用户都是一个Entity
另一方面,将UserCollection
传递到预期BaseCollection
的地方是行不通的:你被驱逐了能够添加一个Entity
,但你需要一个User
——或者换句话说:当你从UserCollection
中获取一个元素时,你可能会在此之后得到一个Entity
,你期望一个User
。
这违反了 Liskov 替换原则,因为无法将实体的其他实现添加到 UserCollection 中。 引用 BaseCollection 的用户不会期望作为 UserCollections 的实现在提供用户以外的实体时爆炸。
我假设UserCollection.add正在取代BaseCollection.add,因为您明确提到了缩小并且没有指定语言。
方法参数应该是逆变的,如果您遵循 Liskov 替换原则,则不应协变。 http://en.wikipedia.org/wiki/Liskov_substitution_principle。
如果BaseCollection
的合约指定其add
方法可以合法地传递从Entity
派生的任何对象,那么继承的add
方法UserCollection
也应该这样做,否则将违反LSP。 如果原始add
方法可以与从Entity
派生的任意对象一起使用,则add
UserCollection
包含仅接受 User
类型的对象的重载(而不是覆盖)不会违反 LSP,尽管重载可能不是特别合适。
如果所讨论的方法不是add
,而是类似于 base 中的 setItem(int index, Entity value)
和派生类中的setItem(int index, User value)
,并且如果合约指定它只保证使用从同一集合中读取的对象,那么只要读取UserCollection
永远不会产生除User
实例之外的任何内容, setItem
方法可以合法地拒绝所有不是User
实例的对象,而不会违反 LSP。 如果setItem
方法要拒绝所有不是user
实例的东西,那么拥有一个只接受user
的重载可能是有用和合适的;即使继承的setItem
方法需要验证value
是否标识了User
的实例,接受该类型参数的重载也不会。 添加此类重载时最大的警告是应避免使用两个未密封的虚拟方法执行相同的操作;如果要添加重载,则可能应该重写并密封基类方法,以便它将传入的参数转换为类型 User
,然后链接到该方法的重载版本。
请注意,数组订阅后一种形式的契约和继承;类型 Animal[]
的变量可以保存对Cat[]
的引用;尝试将任意Animal
存储到标识Cat[]
的Animal[]
中可能会失败,但从Animal[]
中读取的任何Animal
都保证"适合"在同一数组中;这使得代码可以对元素进行排序或排列在任意引用类型的数组中,而不必知道相关数组的类型。