我听说利斯科夫替换原则(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 -芭芭拉利斯科夫
其他回答
利斯科夫替换原理
(固体)
继承子类型化
维基里斯科夫替换原理(LSP)
在子类型中不能加强先决条件。 后置条件不能在子类型中减弱。 超类型的不变量必须保留在子类型中。
子类型不应该要求调用者提供比超类型更多的(先决条件) 子类型不应该为小于超类型的调用者公开(后置条件)
*前置条件+后置条件=函数(方法)类型[Swift函数类型。Swift函数与方法
//Swift function
func foo(parameter: Class1) -> Class2
//function type
(Class1) -> Class2
//Precondition
Class1
//Postcondition
Class2
例子
//C3 -> C2 -> C1
class C1 {}
class C2: C1 {}
class C3: C2 {}
前提条件(如。函数参数类型)可以相同或更弱(力求-> C1) 后置条件(如。函数返回类型)可以相同或更强(力求-> C3) 超类型的不变变量[About]应该保持不变
斯威夫特
class A {
func foo(a: C2) -> C2 {
return C2()
}
}
class B: A {
override func foo(a: C1) -> C3 {
return C3()
}
}
Java
class A {
public C2 foo(C2 a) {
return new C2();
}
}
class B extends A {
@Override
public C3 foo(C2 a) { //You are available pass only C2 as parameter
return new C3();
}
}
行为子类型化
维基里斯科夫替换原理(LSP)
子类型中方法参数类型的逆变性。子类型中方法返回类型的协方差。 子类型中的方法不能引发新的异常,除非它们是超类型的方法引发的异常的子类型。
[方差,协方差,逆变,不变性]
利斯科夫替换原则(来自Mark Seemann的书)指出,我们应该能够在不破坏客户端或实现的情况下,用另一个接口的实现替换一个接口的实现。正是这一原则使我们能够解决未来出现的需求,即使我们今天不能预见它们。
If we unplug the computer from the wall (Implementation), neither the wall outlet (Interface) nor the computer (Client) breaks down (in fact, if it’s a laptop computer, it can even run on its batteries for a period of time). With software, however, a client often expects a service to be available. If the service was removed, we get a NullReferenceException. To deal with this type of situation, we can create an implementation of an interface that does “nothing.” This is a design pattern known as Null Object,[4] and it corresponds roughly to unplugging the computer from the wall. Because we’re using loose coupling, we can replace a real implementation with something that does nothing without causing trouble.
使用指向基类的指针或引用的函数必须能够在不知道它的情况下使用派生类的对象。
当我第一次阅读LSP时,我认为这是一个非常严格的含义,本质上等同于接口实现和类型安全强制转换。这意味着语言本身要么保证LSP,要么不保证LSP。例如,在严格意义上,ThreeDBoard当然可以取代Board,就编译器而言。
在阅读了更多关于LSP的概念之后,我发现LSP的解释通常比这更广泛。
简而言之,对于客户端代码来说,“知道”指针后面的对象是派生类型而不是指针类型的含义并不仅限于类型安全。对LSP的遵守也可以通过探测对象的实际行为进行测试。也就是说,检查对象的状态和方法参数对方法调用结果或从对象抛出的异常类型的影响。
再次回到示例,理论上Board方法可以在ThreeDBoard上很好地工作。然而,在实践中,在不妨碍ThreeDBoard打算添加的功能的情况下,防止客户端可能无法正确处理的行为差异是非常困难的。
掌握了这些知识后,评估LSP粘附性可以成为一个很好的工具,可以确定何时组合机制更适合扩展现有功能,而不是继承。
罗伯特·马丁有一篇关于利斯科夫替换原理的优秀论文。它讨论了可能违反原则的微妙和不那么微妙的方式。
论文的一些相关部分(注意,第二个例子被大量压缩):
A Simple Example of a Violation of LSP One of the most glaring violations of this principle is the use of C++ Run-Time Type Information (RTTI) to select a function based upon the type of an object. i.e.: void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); } Clearly the DrawShape function is badly formed. It must know about every possible derivative of the Shape class, and it must be changed whenever new derivatives of Shape are created. Indeed, many view the structure of this function as anathema to Object Oriented Design. Square and Rectangle, a More Subtle Violation. However, there are other, far more subtle, ways of violating the LSP. Consider an application which uses the Rectangle class as described below: class Rectangle { public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeight() const {return itsHeight;} double GetWidth() const {return itsWidth;} private: double itsWidth; double itsHeight; }; [...] Imagine that one day the users demand the ability to manipulate squares in addition to rectangles. [...] Clearly, a square is a rectangle for all normal intents and purposes. Since the ISA relationship holds, it is logical to model the Square class as being derived from Rectangle. [...] Square will inherit the SetWidth and SetHeight functions. These functions are utterly inappropriate for a Square, since the width and height of a square are identical. This should be a significant clue that there is a problem with the design. However, there is a way to sidestep the problem. We could override SetWidth and SetHeight [...] But consider the following function: void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth } If we pass a reference to a Square object into this function, the Square object will be corrupted because the height won’t be changed. This is a clear violation of LSP. The function does not work for derivatives of its arguments. [...]
Liskov's Substitution Principle(LSP) All the time we design a program module and we create some class hierarchies. Then we extend some classes creating some derived classes. We must make sure that the new derived classes just extend without replacing the functionality of old classes. Otherwise, the new classes can produce undesired effects when they are used in existing program modules. Liskov's Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.
例子:
Below is the classic example for which the Liskov's Substitution Principle is violated. In the example, 2 classes are used: Rectangle and Square. Let's assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don't know the exact what type of object will be returned. But we know it's a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 100
// Violation of Likov's Substitution Principle
class Rectangle {
protected int m_width;
protected int m_height;
public void setWidth(int width) {
m_width = width;
}
public void setHeight(int height) {
m_height = height;
}
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
public int getArea() {
return m_width * m_height;
}
}
class Square extends Rectangle {
public void setWidth(int width) {
m_width = width;
m_height = width;
}
public void setHeight(int height) {
m_width = height;
m_height = height;
}
}
class LspTest {
private static Rectangle getNewRectangle() {
// it can be an object returned by some factory ...
return new Square();
}
public static void main(String args[]) {
Rectangle r = LspTest.getNewRectangle();
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle.
// It assumes that he's able to set the width and height as for the base
// class
System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}
结论: 这个原则只是开闭原则的延伸 意味着我们必须确保新的派生类正在扩展 基类而不改变它们的行为。
参见:开闭原则
对于更好的结构,还有一些类似的概念:约定优于配置