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


当前回答

关于LSP的一个很好的例子(在我最近听到的播客中,Bob叔叔给出了一个例子)是,有时候在自然语言中听起来正确的东西在代码中却不太适用。

在数学中,正方形是长方形。实际上,它是矩形的专门化。“is a”使您想用继承来建模。然而,如果在代码中你从Rectangle派生出Square,那么Square应该可以在任何你想要Rectangle的地方使用。这就导致了一些奇怪的行为。

假设你在你的Rectangle基类上有SetWidth和SetHeight方法;这似乎完全合乎逻辑。然而,如果你的矩形引用指向一个正方形,那么SetWidth和SetHeight没有意义,因为设置一个会改变另一个来匹配它。在这种情况下,Square未能通过矩形的利斯科夫替换测试,并且让Square继承Rectangle的抽象是一个糟糕的抽象。

你们都应该看看其他用励志海报解释的无价的坚实原则。

其他回答

LSP的这种形式太强大了:

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

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

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

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

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

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

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

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

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

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

考虑下面的例子:

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。”然而,事实并非如此——只要孩子不违反父母的契约。

长话短说,让我们留下矩形矩形和正方形,实际的例子,当扩展一个父类时,你必须要么保留确切的父API,要么扩展IT。

假设您有一个基本ItemsRepository。

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

以及扩展它的子类:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

然后,您可以让客户端使用Base ItemsRepository API并依赖它。

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

当用子类替换父类破坏了API的契约时,LSP就被破坏了。

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

你可以在我的课程中学习更多关于编写可维护软件的知识:https://www.udemy.com/enterprise-php/

我想每个人都了解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 -芭芭拉利斯科夫