Liskov替换原则对派生类中的方法签名施加的规则之一是:
子类型中方法参数的对比。
如果我理解正确的话,就是说派生类的重写函数应该允许逆变参数(超类型参数)。但是,我不明白这条规则背后的原因。由于LSP主要讨论动态地将类型绑定到这些子类型(而不是超类型)以实现抽象,所以允许超类型作为派生类中的方法参数对我来说是相当混乱的。我的问题是:
我知道这是一个相当古老的问题,但我认为更现实的使用可能会有所帮助:
class BasicTester
{
TestDrive(Car f)
}
class ExpensiveTester:BasicTester
{
TestDrive(Vehicle v)
}
旧类只能处理汽车类型,而派生类更好,可以处理任何车辆。此外,那些使用“旧”车型的新车型的人也将得到服务。
但是,不能像C#中那样重写。您可以使用委托间接实现这一点:
protected delegate void TestDrive(Car c)
然后可以为其分配一个接受车辆的方法。多亏了逆变技术,它才会起作用。
短语“方法参数的矛盾”可能很简洁,但很模糊。让我们用这个作为例子:
class Base {
abstract void add(Banana b);
}
class Derived {
abstract void add(Xxx? x);
}
现在,“方法参数的逆变”可能意味着派生。add
必须接受任何具有类型Banana
或超类型的对象,例如?超级香蕉
。这是对LSP规则的错误解释。
实际解释是:“派生。add
必须使用类型Banana
声明,就像在Base
中一样,或者使用Banana
的某些超类型声明,例如Fruit
”您选择哪种超类型取决于您。
我相信用这种解释不难看出这条规则是完全合理的。您的子类与父API兼容,但它也可以选择性地覆盖基类没有的额外情况。因此,它是基类的LSP替代品。
在实践中,在子类中拓宽类型是有用的例子并不多。我想这就是为什么大多数语言都懒得实现它。要求严格相同的类型也保留了LSP,只是没有给你在实现LSP的同时可以拥有的全部灵活性。
这里,按照LSP的说法,“派生对象”应该可以用作“基本对象”的替换。
假设您的基础对象有一个方法:
class BasicAdder
{
Anything Add(Number x, Number y);
}
// example of usage
adder = new BasicAdder
// elsewhere
Anything res = adder.Add( integer1, float2 );
这里,“数字”是数字的基本类型的概念,比如数据类型、整数、浮点、双精度等。在i.e.C中不存在这样的东西,但是,我们这里不讨论特定的语言。类似地,仅出于示例目的,“Anything”描述任何类型的无限制值。
让我们考虑一个派生对象,它是“专门”使用复杂的:
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
参数,因为它实际上需要复杂的参数。
另一方面,请注意协变返回类型是可以的:复杂的返回类型将适合任何东西
。
现在,让我们考虑另一种情况:
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。但是是吗?通常不会,但这取决于很多事情。
对比方差规则如何帮助实现数据/过程抽象?
很好。。对我来说很明显。如果您创建了(比如)可交换/可交换/可替换的组件:
基数:简单计算发票金额
然后添加一个新的:
计算不同货币的发票金额
假设它处理欧元和英镑的输入值。以旧货币(比如美元)投入的情况如何?如果您忽略了这一点,那么新组件就不是旧组件的替代品。你不能只是拿出旧的部件,插上新的,然后希望一切都好。系统中的所有其他东西仍然可以发送美元值作为输入。
如果我们创建的新组件是从BASE派生的,那么每个人都可以放心地假设,他们可以在之前需要BASE的任何地方使用它。如果某个地方需要底座,但使用了DER-2,那么我们应该能够在那里插入新的成分。这是LSP。如果我们不能,那么有些东西就坏了:
任何一个使用地点都不仅仅需要基地,事实上需要更多
现在,如果没有任何损坏,我们可以选择一个并替换为另一个,不管是USD还是GBPs,单核还是多核。现在,从上面的一个层面来看,如果不再需要关注特定类型的货币,那么我们成功地将其抽象出来,总体情况会更简单,当然,组件需要以某种方式在内部处理。
如果感觉这对数据/过程抽象没有帮助,那么看看相反的情况:
如果从BASE派生的组件没有遵守LSP,那么当USD中的合法值到达时,它可能会引发错误。或者更糟糕的是,它不会注意到,并将它们作为英镑处理。我们有麻烦了。为了解决这个问题,我们需要修复新组件(遵守BASE的所有要求),或者更改其他相邻组件以遵循新规则,如“现在使用欧元而不是美元,否则加法器将抛出异常”,或者我们需要向大局来解决这个问题,即添加一些分支来检测旧风格的数据,并将它们重定向到旧组件。我们只是“泄露”复杂性给邻居(也许我们强迫他们打破SRP),或者我们让“大局”更加复杂(更多适配器、条件、分支,...)。
来自维基百科, Liskov的行为子类型概念定义了对象的可替代性概念;也就是说,如果S是T的子类型,则程序中T类型的对象可以替换为S类型的对象,而不改变该程序的任何期望属性(例如正确性)。 假设以下类层次结构: 基本抽象类-。它有一个只读属性,在后继程序中被重写。 基类的继承者-,它重写并返回灰色。 Cat的继任者-,它覆盖并返回带条纹的。 然后我们声明一个方法,参数类型为(不是)。 向该方法发送
LSP定义指出,如果S是T的子类型,则程序中T类型的对象可以替换为S类型的对象,而不改变该程序的任何期望属性。 子类型中的前提条件不能加强 例如,我有下面的类,这是违反(在子类型中不能加强前提条件)。我正试图把我的头绕在这上面,请有人提供一个好的例子来理解它。
我试图通过反复阅读维基百科条目来确定我对上述原则的理解。 撇开仍然让我悲伤的协变和逆变的概念不谈,wikipedia还提到超类型的不变量必须保留在子类型和历史约束或历史规则中。基于最后两个概念,我提出了一个小例子: 所以我的问题是:基于上述两个概念,我用这个例子是否违反了原则?若否,原因为何? 事先非常感谢。
假设我有一个抽象类鸟,它的一个函数是飞(int高度)。 我有许多不同的鸟类,每个类都有自己不同的飞的实现,这个函数在整个应用程序中被广泛使用。 有一天,我的老板来了,要求我添加一只鸭子,它做其他鸟类所做的一切,只是它不飞,而是在应用程序的池塘里游泳。 将duck添加为bird的子类型违反了Liskov替换规则,因为在调用duck时。我们要么抛出异常,要么什么也不做,要么违反正确性原则。 在牢记坚实
我在理解这两个原则时有些困难。这是一个有点长的阅读问题,所以要有耐心。 假设我们有一个类 和接口 然后我们创建了两个子类 现在我们将使implements 并在儿童课程中做出改变 并为该结构创建测试函数(如下LSP): 在这里我想停下来,因为实际上在下一步我被绊倒了。如果我们要创建第三个类? 圆没有边,所以为这个孩子实现听起来很可笑。好的,我们可以只将实现移到四边形和三角形,但在这种情况下LSP将
问题内容: 我听说Liskov替代原理(LSP)是面向对象设计的基本原理。它是什么?有哪些使用示例? 问题答案: 一个很好的例子说明了LSP(我最近听到的一个播客中的Bob叔叔给了LSP),就是有时候听起来有些自然语言在代码中不太起作用。 在数学中,是。实际上,它是矩形的一种特殊形式。“是”使您想使用继承对其进行建模。但是,如果在您编写的代码中Square派生自Rectangle,则aSquare