我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
我想每个人都了解LSP在技术上是什么:你基本上希望能够从子类型细节中抽象出来,并安全地使用超类型。
所以利斯科夫有3条基本规则:
Signature Rule : There should be a valid implementation of every operation of the supertype in the subtype syntactically. Something a compiler will be able to check for you. There is a little rule about throwing fewer exceptions and being at least as accessible as the supertype methods. Methods Rule: The implementation of those operations is semantically sound. Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more. Stronger Postconditions: They should produce a subset of the output the supertype methods produced. Properties Rule : This goes beyond individual function calls. Invariants : Things that are always true must remain true. Eg. a Set's size is never negative. Evolutionary Properties : Usually something to do with immutability or the kind of states the object can be in. Or maybe the object only grows and never shrinks so the subtype methods shouldn't make it.
所有这些属性都需要保留,并且额外的子类型功能不应该违反超类型属性。
如果这三件事都处理好了,那么您就从底层的东西中抽象出来了,并且您正在编写松散耦合的代码。
来源:程序开发在Java -芭芭拉利斯科夫
其他回答
它指出,如果C是E的子类型,则E可以替换为C类型的对象,而不会改变或破坏程序的行为。简单地说,派生类应该可以替代它们的父类。例如,如果一个农民的儿子是农民,那么他可以代替他的父亲工作,但如果一个农民的儿子是板球运动员,那么他就不能代替他的父亲工作。
违反的例子:
public class Plane{
public void startEngine(){}
}
public class FighterJet extends Plane{}
public class PaperPlane extends Plane{}
在给定的例子中,fighter和PaperPlane类都扩展了包含startEngine()方法的Plane类。所以很明显,战斗机可以启动引擎,但纸飞机不能,所以它破坏LSP。
PaperPlane类虽然扩展了Plane类,但应该可以替代Plane类,但它不是Plane实例可以被替换的合格实体,因为纸飞机不能启动引擎,因为它没有引擎。好的例子是,
受人尊敬的例子:
public class Plane{
}
public class RealPlane{
public void startEngine(){}
}
public class FighterJet extends RealPlane{}
public class PaperPlane extends Plane{}
一些补充:我想知道为什么没有人写基类的不变量、前提条件和后置条件,这些派生类必须遵守。 对于派生类D来说,基类B完全可转换,类D必须服从某些条件:
基类的内变体必须由派生类保留 派生类不能加强基类的先决条件 派生类不能削弱基类的后置条件。
因此派生类必须知道基类施加的上述三个条件。因此,子类型的规则是预先确定的。这意味着,只有当子类型遵守某些规则时,才应该遵守'IS A'关系。这些规则,以不变量、前置条件和后置条件的形式,应该由正式的“设计契约”来决定。
关于这个问题的进一步讨论可以在我的博客:利斯科夫替换原理
设q(x)是关于类型为T的x的对象的可证明属性,那么q(y)对于类型为S的对象y应该是可证明的,其中S是T的子类型。
实际上,公认的答案并不是利斯科夫原理的反例。正方形自然是一个特定的矩形,因此从类矩形继承是完全有意义的。你只需要以这样的方式实现它:
@Override
public void setHeight(double height) {
this.height = height;
this.width = height; // since it's a square
}
@Override
public void setWidth(double width) {
setHeight(width);
}
所以,提供了一个很好的例子,然而,这是一个反例:
class Family:
-- getChildrenCount()
class FamilyWithKids extends Family:
-- getChildrenCount() { return childrenCount; } // always > 0
class DeadFamilyWithKids extends FamilyWithKids:
-- getChildrenCount() { return 0; }
-- getChildrenCountWhenAlive() { return childrenCountWhenAlive; }
在这个实现中,DeadFamilyWithKids不能从FamilyWithKids继承,因为getChildrenCount()返回0,而从FamilyWithKids它应该总是返回大于0的值。
让我们用Java来说明:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
class Car extends TransportationDevice
{
@Override
void startEngine() { ... }
}
这里没有问题,对吧?汽车绝对是一种交通工具,在这里我们可以看到它重写了其超类的startEngine()方法。
让我们添加另一个交通工具:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
现在一切都不按计划进行了!是的,自行车是一种交通工具,但是,它没有发动机,因此,startEngine()方法不能实现。
这些都是违反利斯科夫代换法的问题 原则导致,他们通常可以被一个公认的 方法,该方法什么也不做,甚至不能实现。
这些问题的解决方案是一个正确的继承层次结构,在我们的例子中,我们将通过区分带引擎和不带引擎的运输设备类别来解决问题。尽管自行车是一种交通工具,但它没有发动机。在这个例子中,我们对交通工具的定义是错误的。它不应该有引擎。
我们可以像下面这样重构TransportationDevice类:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
现在我们可以为非机动设备扩展TransportationDevice。
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
并为机动设备扩展TransportationDevice。这里更适合添加Engine对象。
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
因此,我们的Car类变得更加专门化,同时坚持利斯科夫替换原则。
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
我们的Bicycle类也遵循利斯科夫替换原理。
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.
因此,可调整大小的正方形不是可调整大小的矩形。