我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
使用LSP的一个重要例子是在软件测试中。
如果我有一个类a,它是B的一个符合lsp的子类,那么我可以重用B的测试套件来测试a。
为了完全测试子类A,我可能需要添加更多的测试用例,但至少我可以重用所有超类B的测试用例。
实现这一点的一种方法是构建McGregor所说的“用于测试的并行层次结构”:我的ATest类将继承BTest。然后需要某种形式的注入来确保测试用例使用类型A的对象而不是类型B的对象(一个简单的模板方法模式就可以了)。
注意,对所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否与lsp兼容的方法。因此,人们也可以主张应该在任何子类的上下文中运行超类测试套件。
另请参阅对Stackoverflow问题的回答“我是否可以实现一系列可重用测试来测试接口的实现?”
其他回答
让我们用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() { ... }
}
这里有一个清单来确定你是否违反了利斯科夫法则。
如果你违反了以下项目之一->,你违反了里斯科夫。 如果你不违反任何->不能得出任何结论。
检查表:
No new exceptions should be thrown in derived class: If your base class threw ArgumentNullException then your sub classes were only allowed to throw exceptions of type ArgumentNullException or any exceptions derived from ArgumentNullException. Throwing IndexOutOfRangeException is a violation of Liskov. Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int to be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken. Post-conditions cannot be weakened: Assume your base class required all connections to the database should be closed before the method returned. In your sub-class you overrode that method and left the connection open for further reuse. You have weakened the post-conditions of that method. Invariants must be preserved: The most difficult and painful constraint to fulfill. Invariants are sometimes hidden in the base class and the only way to reveal them is to read the code of the base class. Basically you have to be sure when you override a method anything unchangeable must remain unchanged after your overridden method is executed. The best thing I can think of is to enforce these invariant constraints in the base class but that would not be easy. History Constraint: When overriding a method you are not allowed to modify an unmodifiable property in the base class. Take a look at these code and you can see Name is defined to be unmodifiable (private set) but SubType introduces new method that allows modifying it (through reflection): public class SuperType { public string Name { get; private set; } public SuperType(string name, int age) { Name = name; Age = age; } } public class SubType : SuperType { public void ChangeName(string newName) { var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName); } }
还有2项:方法参数的逆变性和返回类型的协方差。但这在c#中是不可能的(我是c#开发人员),所以我不关心它们。
以Board数组的形式实现ThreeDBoard会有用吗?
也许你想把不同平面上的ThreeDBoard切片作为一个板。在这种情况下,您可能希望为Board抽象出一个接口(或抽象类),以允许多种实现。
就外部接口而言,您可能希望为TwoDBoard和ThreeDBoard提取一个Board接口(尽管上述方法都不适合)。
可替代性是面向对象编程中的一个原则,它指出,在计算机程序中,如果S是T的子类型,那么类型T的对象可以被类型S的对象替换
让我们用Java做一个简单的例子:
不好的例子
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
鸭子能飞,因为它是鸟,但这个呢:
public class Ostrich extends Bird{}
鸵鸟是一种鸟,但它不能飞,鸵鸟类是鸟类的一个子类,但它不应该能够使用fly方法,这意味着我们打破了LSP原则。
很好的例子
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
LSP的这种形式太强大了:
如果对于每个类型为S的对象o1,都有一个类型为T的对象o2,使得对于所有用T定义的程序P,当o1取代o2时,P的行为不变,那么S是T的子类型。
这基本上意味着S是t的另一个完全封装的实现,我可以大胆地认为性能是P行为的一部分……
因此,基本上,任何延迟绑定的使用都违反了LSP。当我们用一种类型的对象替换另一种类型的对象时,获得不同的行为是OO的全部意义所在!
维基百科引用的公式更好,因为属性取决于上下文,并不一定包括程序的整个行为。