Liskov Substitution Principle对派生类中的方法签名施加的规则之一是:
子类型中方法参数的反演。
如果我理解正确,那就是说派生类的重写函数应该允许反变量参数(超类型参数)。但是,我无法理解这条规则背后的原因。由于LSP主要是关于动态地将类型与子类型(而不是超类型)绑定以实现抽象,因此允许超类型作为派生类中的方法参数对我来说非常困惑。 我的问题是:
答案 0 :(得分:4)
短语"方法论证的逆转"可能很简洁,但它含糊不清。我们以此为例:
class Base {
abstract void add(Banana b);
}
class Derived {
abstract void add(Xxx? x);
}
现在,"方法论证的逆转"可能意味着Derived.add
必须接受任何具有Banana
类型或超类型的对象,例如? super Banana
。这是对LSP规则的不正确的解释。
实际解释是:" Derived.add
必须使用Banana
类型声明,就像Base
一样,或Banana
的某些超类型声明Fruit
&#34。你选择哪种超类型取决于你。
我相信使用这种解释不难看出规则是完全合理的。您的子类与父API兼容,但它也可以选择性地涵盖基类没有的额外情况。因此它的LSP可替代基类。
在实践中,没有很多例子可以在子类中扩展这种类型。我认为这就是为什么大多数语言都不愿意实现它的原因。要求严格相同的类型也可以保留LSP,但是在获得LSP的同时,并没有给你足够的灵活性。
答案 1 :(得分:3)
这里,按照LSP的说法,“派生对象”应该可以用作“基础对象”的替代。
假设你的基础对象有一个方法:
class BasicAdder
{
Anything Add(Number x, Number y);
}
// example of usage
adder = new BasicAdder
// elsewhere
Anything res = adder.Add( integer1, float2 );
这里,“Number”是类数数据类型,整数,浮点数,双精度等的基类型的概念。在C ++中不存在这样的东西,但是,我们在这里不讨论特定的语言。同样,仅仅出于举例的目的,“Anything”描述了任何类型的无限制值。
让我们考虑一个“专门”使用Complex的派生对象:
class ComplexAdder
{
Complex Add(Complex x, Complex y);
}
// example of usage
adder = new ComplexAdder
// elsewhere
Anything res = adder.Add( integer1, float2 ); // FAIL
因此,我们刚刚破坏了LSP:它不能用作原始对象的替代品,因为它无法接受integer1, float2
参数,因为它实际上需要复杂参数。 / p>
另一方面,请注意协变返回类型是正确的:复杂的返回类型适合Anything
。
现在,让我们考虑另一种情况:
class SupersetComplexAdder
{
Anything Add(ComplexOrNumberOrShoes x, ComplexOrNumberOrShoes y);
}
// example of usage
adder = new SupersetComplexAdder
// elsewhere
Anything res = adder.Add( integer1, float2 ); // WIN
现在一切正常,因为无论谁使用旧对象,现在也能够使用新对象,对使用点没有任何影响。
当然,并不总是可以创建这样的“联合”或“超集”类型,特别是在数字方面,或者在某些自动类型转换方面。但是,我们不是在讨论特定的编程语言。总体思路很重要。
值得注意的是,您可以在各种“级别”上遵守或破坏LSP
class SmartAdder
{
Anything Add(Anything x, Anything y)
{
if(x is not really Complex) throw error;
if(y is not really Complex) throw error;
return complex-add(x,y)
}
}
它肯定看起来像在类/方法签名级别符合LSP。但是吗?通常不会,但这取决于很多事情。
Contravariance规则如何有助于实现数据/过程抽象?
对我来说很明显。如果你创建了可以交换/可交换/可替换的说,组件:
然后添加一个新的:
让我们说它处理欧元和英镑的输入值。那么输入旧货币呢,比如美元?如果省略,那么新组件不是替代旧组件。您不能只取出旧组件并插入新组件并希望一切正常。系统中的所有其他内容仍可能将美元值作为输入发送。
如果我们创建从BASE派生的新组件,那么每个人都可以安全地假设他们可以在之前需要BASE的任何地方使用它。如果某个地方需要BASE,但是使用了DER-2,那么我们应该能够在那里插入新的组件。这是LSP。如果我们做不到,那么事情就会被打破:
现在,如果没有任何损失,我们可以拿一个并替换另一个,无论是美元还是英镑,还是单核或多核。现在,看一级以上的大局,如果不再需要关心特定类型的货币,那么我们成功地将其抽象化,大局将更简单,当然,组件需要在内部处理不知。
如果这对于数据/过程抽象没有帮助,那么请看相反的情况:
如果从BASE派生的组件不遵守LSP,那么当美元的合法值到达时,它可能会引发错误。或者更糟糕的是,它不会注意到并将它们作为GBP处理。我们出现了问题。要解决这个问题,我们需要修复新组件(以遵守BASE的所有要求),或者更改其他邻居组件以遵循新规则,例如“现在使用EUR而不是USD,或者Adder会抛出异常”,或者我们需要在大图中添加东西来处理它,即添加一些分支,它们将检测旧式数据并将它们重定向到旧组件。我们只是“泄露”了邻居的复杂性(也许我们强迫他们打破SRP)或者我们让“大局”变得更加复杂(更多的适配器,条件,分支......)。
答案 2 :(得分:1)
我知道这是一个很老的问题,但我认为更现实的生活用途可能会有所帮助:
class BasicTester
{
TestDrive(Car f)
}
class ExpensiveTester:BasicTester
{
TestDrive(Vehicle v)
}
旧类只能使用Car类型,而派生类则更好,并且可以处理任何Vehicle。此外,还将为那些使用“旧”汽车类型的新班级的人提供服务。
但是,您不能像C#中那样重写。 您可以使用委托间接实现该目标:
protected delegate void TestDrive(Car c)
然后可以为分配一个接受Vehicle的方法。谨防矛盾会起作用。