我听说利斯科夫替换原则(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();

这里的例子

其他回答

以Board数组的形式实现ThreeDBoard会有用吗?

也许你想把不同平面上的ThreeDBoard切片作为一个板。在这种情况下,您可能希望为Board抽象出一个接口(或抽象类),以允许多种实现。

就外部接口而言,您可能希望为TwoDBoard和ThreeDBoard提取一个Board接口(尽管上述方法都不适合)。

利科夫替换原则指出,如果程序模块使用基类,则基类的引用可以被派生类替换,而不会影响程序模块的功能。

派生类型必须能够完全替代它们的基类型。

示例- java中的协变返回类型。

该原则由Barbara Liskov在1987年提出,并通过关注超类及其子类型的行为来扩展开闭原则。

当我们考虑违反它的后果时,它的重要性就变得显而易见了。考虑一个使用以下类的应用程序。

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

想象一下,有一天,客户要求除了矩形之外还能操作正方形。因为正方形是矩形,所以square类应该派生自rectangle类。

public class Square : Rectangle
{
} 

然而,这样做会遇到两个问题:

一个正方形不需要从矩形继承高度和宽度变量,如果我们必须创建成千上万个正方形对象,这可能会造成严重的内存浪费。 从矩形继承的width和height setter属性不适用于正方形,因为正方形的宽度和高度是相同的。 为了将height和width设置为相同的值,我们可以创建两个新属性,如下所示:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

现在,当有人设置一个正方形物体的宽度时,它的高度将相应地改变,反之亦然。

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

让我们继续考虑另一个函数:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

如果我们将一个方形对象的引用传递给这个函数,我们将违反LSP,因为该函数对其参数的导数不起作用。属性width和height不是多态的,因为它们在矩形中没有被声明为虚的(正方形对象将被损坏,因为高度不会被改变)。

然而,通过将setter属性声明为virtual,我们将面临另一个违反,即OCP。事实上,派生类正方形的创建会导致基类矩形的变化。

以下是这篇文章的摘录,很好地澄清了事情:

(. .为了理解一些原则,重要的是要意识到它什么时候被违反了。这就是我现在要做的。

违反这一原则意味着什么?它意味着对象不履行用接口表示的抽象所施加的契约。换句话说,这意味着您错误地识别了抽象。

考虑下面的例子:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

是否违反LSP?是的。这是因为帐户合同告诉我们帐户将被提取,但情况并非总是如此。那么,我该怎么做才能解决这个问题呢?我只是修改了合同:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà,现在合同已得到满足。

这种微妙的违反通常会使客户有能力区分所使用的具体对象之间的差异。例如,给定第一个Account的契约,它看起来像下面这样:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

而且,这自动违反了开闭原则(即取款要求)。因为你永远不知道如果违反合同的对象没有足够的钱会发生什么。它可能什么都不返回,可能会抛出异常。所以你必须检查它是否hasEnoughMoney()——这不是接口的一部分。因此这种强制的依赖于具体类的检查违反了OCP。

这一点也解决了我经常遇到的关于LSP违反的误解。它说:“如果父母的行为在孩子身上改变了,那么它就违反了LSP。”然而,事实并非如此——只要孩子不违反父母的契约。

Liskov替换原理(LSP, LSP)是面向对象编程中的一个概念,它指出:

函数使用指针或 基类的引用必须是 能够使用派生类的对象 在不知不觉中。

LSP的核心是关于接口和契约,以及如何决定何时扩展一个类,还是使用另一种策略(如组合)来实现您的目标。

我所见过的说明这一点的最有效的方法是《Head First OOA&D》。它们呈现的场景是,你是一名致力于为策略游戏构建框架的项目开发者。

他们展示了一个类,它代表一个板子,看起来像这样:

所有的方法都以X和Y坐标作为参数来定位tile在二维tile数组中的位置。这将允许游戏开发者在游戏过程中管理棋盘上的单位。

这本书继续改变了要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏。因此引入了一个ThreeDBoard类,它扩展了Board。

乍一看,这似乎是个不错的决定。Board提供了高度和宽度属性,ThreeDBoard提供了Z轴。

当你看到从董事会继承的所有其他成员时,它就失效了。AddUnit, GetTile, GetUnits等方法在Board类中都采用X和Y参数,但ThreeDBoard也需要Z参数。

因此,您必须使用Z参数再次实现这些方法。Z参数没有Board类的上下文,从Board类继承的方法失去了意义。试图使用ThreeDBoard类作为其基类Board的代码单元将非常不走运。

也许我们应该另想办法。ThreeDBoard应该由Board对象组成,而不是扩展Board。Z轴上每单位一个板子对象。

这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP。