我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?


当前回答

让我试着考虑一个接口:

interface Planet{
}

这是由类实现的:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

你将使用地球作为:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

现在再考虑一个扩展到地球的阶级:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

根据LSP的说法,你应该可以用LiveablePlanet代替Earth,而且它不会破坏你的系统。如:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

这里的例子

其他回答

假设我们在代码中使用了一个矩形

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

在几何课上,我们学过正方形是一种特殊类型的矩形,因为它的长宽相等。让我们根据下面的信息创建一个Square类:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

如果我们在第一个代码中将矩形替换为正方形,那么它将会中断:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

这是因为正方形有一个我们在矩形类中没有的新前提条件:width == height。根据LSP,矩形实例应该被矩形子类实例替代。这是因为这些实例通过了矩形实例的类型检查,因此它们将在代码中导致意外错误。

这是wiki文章中“在子类型中不能加强先决条件”部分的一个例子。因此,总而言之,违反LSP可能会在某些时候导致代码错误。

LSP的这种形式太强大了:

如果对于每个类型为S的对象o1,都有一个类型为T的对象o2,使得对于所有用T定义的程序P,当o1取代o2时,P的行为不变,那么S是T的子类型。

这基本上意味着S是t的另一个完全封装的实现,我可以大胆地认为性能是P行为的一部分……

因此,基本上,任何延迟绑定的使用都违反了LSP。当我们用一种类型的对象替换另一种类型的对象时,获得不同的行为是OO的全部意义所在!

维基百科引用的公式更好,因为属性取决于上下文,并不一定包括程序的整个行为。

简单来说,LSP是指同一超类的对象应该能够在不破坏任何东西的情况下相互交换。

例如,如果我们有一个从Animal类派生的Cat和Dog类,那么任何使用Animal类的函数都应该能够使用Cat或Dog,并且行为正常。

让我们用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() { ... }
}

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.
    }
}

结论: 这个原则只是开闭原则的延伸 意味着我们必须确保新的派生类正在扩展 基类而不改变它们的行为。

参见:开闭原则

对于更好的结构,还有一些类似的概念:约定优于配置