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


当前回答

到目前为止,我发现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。

利斯科夫替换原理

(固体)

继承子类型化

维基里斯科夫替换原理(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)

子类型中方法参数类型的逆变性。子类型中方法返回类型的协方差。 子类型中的方法不能引发新的异常,除非它们是超类型的方法引发的异常的子类型。

[方差,协方差,逆变,不变性]

LSP说“对象应该被它们的子类型替换”。 另一方面,这一原则指向

子类永远不应该破坏父类的类型定义。

通过以下示例,可以更好地理解LSP。

没有太阳能发电:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

LSP修复:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

LSP是关于类的契约的规则:如果基类满足契约,则LSP派生的类也必须满足该契约。

在Pseudo-python

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

如果每次在派生对象上调用Foo,它给出的结果与在Base对象上调用Foo完全相同,只要arg是相同的。

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

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

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

考虑下面的例子:

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