Liskov替换原则指出,子类型应该可以替换该类型(不改变程序的正确性)。
我已经读过正方形/矩形的例子,但是我认为一个关于车辆的例子会让我更好地理解这个概念。
Liskov替换原理指出,具有特定接口的对象可以被实现相同接口的不同对象替换,同时保留原始程序的所有正确性。这意味着不仅接口必须具有完全相同的类型,而且行为也必须保持正确。
在汽车中,你应该能够用不同的零件替换零件,汽车会继续工作。假设你的旧收音机没有数字调谐器,但是你想听高清收音机,所以你买了一个有高清接收器的新收音机。你应该可以把旧收音机拿出来,插上新收音机,只要它有相同的接口。从表面上看,这意味着连接收音机和汽车的电插头需要与旧收音机的形状相同。如果汽车的插头是矩形的,有15个引脚,那么新收音机的插孔需要是矩形的,也有15个引脚。
但除了机械配合之外,还有其他考虑因素:插头上的电气行为也必须相同。如果旧收音机接头上的针脚1为12V,则新收音机接头上的针脚1也必须为12V。如果新收音机的针脚1是“左扬声器输出”针脚,则收音机可能短路或熔断保险丝。这显然违反了LSP。
你也可以考虑一个降级的情况:假设你昂贵的收音机死了,你只能负担得起AM收音机。它没有立体声输出,但它有与现有收音机相同的接口。假设该规范的引脚3为左扬声器输出,引脚4为右扬声器输出。如果您的调幅收音机同时在插脚3和4上播放单声道信号,您可以说它的行为是一致的,这将是一个可接受的替代。但是,如果您的新AM收音机仅在引脚3上播放音频,而在引脚4上不播放音频,则声音会不平衡,这可能不是一个可接受的替代品。这种情况也会违反LSP,因为虽然您可以听到声音,并且没有保险丝熔断,但收音机不符合接口的全部规格。
图片我搬家时想租一辆车。我打电话给租赁公司,问他们有什么型号的。他们告诉我,我会得到下一辆可用的车:
public class CarHireService {
public Car hireCar() {
return availableCarPool.getNextCar();
}
}
但他们给了我一本小册子,告诉我他们所有的车型都有以下特点:
public interface Car {
public void drive();
public void playRadio();
public void addLuggage();
}
这听起来正是我要找的,所以我订了一辆车
public class FormulaOneCar implements Car {
public void drive() {
//Code to make it go super fast
}
public void addLuggage() {
throw new NotSupportedException("No room to carry luggage, sorry.");
}
public void playRadio() {
throw new NotSupportedException("Too heavy, none included.");
}
}
我不高兴,因为他们的宣传手册基本上欺骗了我——一级方程式赛车是否有一个看起来可以装行李但无法打开的假行李箱无关紧要,这对搬家没用!
如果有人告诉我“我们所有的车都是这样做的”,那么给我的任何车都应该这样做。如果我不能相信他们手册中的细节,那就没用了。这就是Liskov替换原理的本质。
对我来说,1996年鲍勃叔叔(罗伯特·C·马丁饰)的这句话总结了LSP的最好之处:
使用指向基类的指针或引用的函数必须能够在不知道的情况下使用派生类的对象。
最近,作为基于(通常是抽象的)基类/超类的子类的继承抽象的替代方法,我们还经常使用接口进行多态抽象。LSP对消费者和抽象的实现都有影响:
LSP合规
下面是一个使用接口IVehile
的示例,该接口可以有多个实现(或者,您可以用几个子类替换抽象基类的接口-效果相同)。
interface IVehicle
{
void Drive(int miles);
void FillUpWithFuel();
int FuelRemaining {get; } // C# syntax for a readable property
}
i车辆
消费者的这种实现保持在LSP的范围内:
void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
...
// Knows only about the interface. Any IVehicle is supported
aVehicle.Drive(50);
}
明显违规-运行时类型切换
这里有一个违反LSP的例子,使用RTTI和向下广播——鲍勃叔叔称之为“明显的违反”:
void MethodWhichViolatesLSP(IVehicle aVehicle)
{
if (aVehicle is Car)
{
var car = aVehicle as Car;
// Do something special for car - this method is not on the IVehicle interface
car.ChangeGear();
}
// etc.
}
违反的方法超出了收缩的IVehi le
接口,并为接口的已知实现(或子类,如果使用继承而不是接口)破解特定路径。鲍勃叔叔还解释说,使用类型切换行为的LSP违规行为通常也违反了打开和关闭原则,因为需要不断修改函数以适应新的子类。
违规-先决条件由子类型强化
另一个违反的例子是“先决条件被子类型加强”:
public abstract class Vehicle
{
public virtual void Drive(int miles)
{
Assert(miles > 0 && miles < 300); // Consumers see this as the contract
}
}
public class Scooter : Vehicle
{
public override void Drive(int miles)
{
Assert(miles > 0 && miles < 50); // ** Violation
base.Drive(miles);
}
}
在这里,Scooter子类试图违反LSP,因为它试图加强(进一步约束)基类Drive
方法上的前提条件,该miles
类似地,子类型可能不会削弱(即放松)后条件。
(C#中代码契约的用户会注意到,前置条件和后置条件必须通过
ContractClassFor
类放置在接口上,并且不能放置在实现类中,从而避免违反)
微妙违反-子类滥用接口实现
一个
更微妙的
违反(也是鲍勃叔叔的术语)可以用一个可疑的派生类来实现接口:
class ToyCar : IVehicle
{
public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
public int FuelRemaining {get {return 0;}}
}
在这里,无论行驶多远的
丰田汽车
,剩余的燃料将始终为零,这将是令人惊讶的IVEHICLE
接口的用户(即无限MPG消耗-永久运动?)。在这种情况下,问题是,尽管玩具汽车
已经实现了接口的所有要求,但玩具汽车
本质上并不是一个真正的IVehi le
,只是接口的橡皮图章。
防止接口或抽象基类以这种方式被滥用的一种方法是确保在接口/抽象基类上提供一组良好的单元测试,以测试所有实现是否满足预期(和任何假设)。单元测试在记录典型用法方面也很出色。e、 g.此
NUnit理论
将拒绝ToyCar
进入您的生产代码库:
[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
vehicle.FillUpWithFuel();
Assert.IsTrue(vehicle.FuelRemaining > 0);
int fuelBeforeDrive = vehicle.FuelRemaining;
vehicle.Drive(20); // Fuel consumption is expected.
Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}
编辑,Re:OpenDoor
开门听起来是一个完全不同的问题,因此需要相应地分开(即实心的“S”和“i”),例如。
添加一个单独的接口
IDoor
,然后像Car
和Truck
这样的车辆将同时实现IVehicle
和IDoor
接口,但是Scooter
和摩托车
将只实现IVehicle
。
在所有情况下,为了避免违反LSP,需要这些接口对象的代码不应该向下转换接口以访问额外的功能。代码应该选择它需要的适当的最小接口/(超级)类,并坚持只在该接口上使用收缩的功能。
Liskov替代原则要求 子类型中的前提条件不能加强 任何人都可以发布一个违反上述每一点的例子和另一个解决这些问题的例子吗?
问题内容: 我听说Liskov替代原理(LSP)是面向对象设计的基本原理。它是什么?有哪些使用示例? 问题答案: 一个很好的例子说明了LSP(我最近听到的一个播客中的Bob叔叔给了LSP),就是有时候听起来有些自然语言在代码中不太起作用。 在数学中,是。实际上,它是矩形的一种特殊形式。“是”使您想使用继承对其进行建模。但是,如果在您编写的代码中Square派生自Rectangle,则aSquare
我试图通过反复阅读维基百科条目来确定我对上述原则的理解。 撇开仍然让我悲伤的协变和逆变的概念不谈,wikipedia还提到超类型的不变量必须保留在子类型和历史约束或历史规则中。基于最后两个概念,我提出了一个小例子: 所以我的问题是:基于上述两个概念,我用这个例子是否违反了原则?若否,原因为何? 事先非常感谢。
问题内容: 我的情况与Code Complete中Steve McConnell 提到的情况非常相似。我唯一的问题是基于车辆,而三轮车恰好是根据法律,属于汽车。到目前为止,汽车只有四个轮子。无论如何,我的域都不必要地复杂,因此很容易遵循下面的cats示例。 对重写例程并且在派生例程中不执行任何操作的类要保持怀疑。这通常表明基类的设计存在错误。例如,假设您有一个Cat类和一个例程Scratch(),
来自维基百科, Liskov的行为子类型概念定义了对象的可替代性概念;也就是说,如果S是T的子类型,则程序中T类型的对象可以替换为S类型的对象,而不改变该程序的任何期望属性(例如正确性)。 假设以下类层次结构: 基本抽象类-。它有一个只读属性,在后继程序中被重写。 基类的继承者-,它重写并返回灰色。 Cat的继任者-,它覆盖并返回带条纹的。 然后我们声明一个方法,参数类型为(不是)。 向该方法发送
有人能告诉我下面的例子是否违反了LSP吗? 我有一个例子: 和子类: 和主类: 在此示例中,子类添加名为 的新属性,并通过对其自己的属性 进行附加检查来覆盖方法。 在main方法中,我创建了2个对象。第一个是类型的对象,第二个是类型的对象。 当验证人员时,因为所有前提条件都是正确的,所以它是正确的,但是对于员工,它将抛出< code > IllegalArgumentException ,因为它与