这个前提条件是否违反了Liskov替代原则?

问题描述 投票:0回答:1

我有3个班。Account, CappedAccount, UserAccount,

CappedAccountUserAccount 两者都延伸 Account.

Account 包含以下内容。

abstract class Account {
   ...
   /**
   * Attempts to add money to account.
   */
   public void add(double amount) {
      balance += amount;
   }
}

CappedAccount 覆盖这个行为。

public class CappedAccount extends Account {
   ...
   @Override
   public void add(double amount) {
      if (balance + amount > cap) { // New Precondition
         return;
      }
      balance += amount;
   }
}

UserAccount 不覆盖任何来自 Account,所以不需要说明。

我的问题是,是否 CappedAccount#add 违反LSP,如果违反,我如何设计才能符合LSP。

例如,是否 add()CappedAccount 算不算 "加强前提条件"?

java oop design-patterns solid-principles
1个回答
2
投票

TLDR;

if (balance + amount > cap) {
    return;
}

非先决条件 不过 不变的,因此并不违反(自己的)利斯科夫代换原理。

现在,实际的答案。

一个真正的前提条件是(伪代码)。

[requires] balance + amount <= cap

你应该能够执行这个先决条件,也就是检查这个条件,如果不符合,就提出一个错误。如果你真的执行了这个前提条件,你会看到LSP被违反了。

Account a = new Account(); // suppose it is not abstract
a.add(1000); // ok

Account a = new CappedAccount(100); // balance = 0, cap = 100
a.add(1000); // raise an error !

子类型的行为应该和它的超类型一样(见下文)。

"加强 "前提条件的唯一方法是加强不变式。因为这个不变式应该是真 前后 每个方法调用。LSP不会被加强的不变式所违反(靠他自己),因为不变式是给定的 免费 方法调用前:它在初始化时为真,因此在第一次方法调用前为真。因为它是一个不变式,所以在第一次方法调用后是真。而一步一步的,在下一次方法调用之前总是真(这是一个数学归纳......)。

class CappedAccount extends Account {
    [invariant] balance <= cap
}

这个不变式应该在方法调用前后都是真。

@Override
public void add(double amount) {
    assert balance <= cap;
    // code
    assert balance <= cap;
}

你如何实现这一点?add 方法?你有一些选择。这一个是确定的。

@Override
public void add(double amount) {
    assert balance <= cap;
    if (balance + amount <= cap) {
        balance += cap;
    }
    assert balance <= cap;
}

嘿,你就是这么做的!(有一点不同:这个有一个退出来检查不变式。)

这个也是,但语义不同。

@Override
public void add(double amount) {
    assert balance <= cap;
    if (balance + amount > cap) {
        balance = cap;
    } else {
        balance += cap;
    }
    assert balance <= cap;
}

这个也是,但语义是荒谬的(或者是一个封闭的账户?)。

@Override
public void add(double amount) {
    assert balance <= cap;
    // do nothing
    assert balance <= cap;
}

好吧,你加了一个不变量,而不是一个前提条件,这就是为什么LSP没有被违反的原因。答案结束。


但是......这并不令人满意。add "试图将钱添加到账户"。我想知道是否成功!!!!!!!!!!?让我们在基类中试试这个。

/**
* Attempts to add money to account.
* @param amount  the amount of money
* @return True if the money was added.
*/
public boolean add(double amount) {
    [requires] amount >= 0
    [ensures] balance = (result && balance == old balance + amount) || (!result && balance == old balance)
}

然后用不变式来实现。

/**
* Attempts to add money to account.
* @param amount  the amount of money
* @return True is the money was added.
*/
public boolean add(double amount) {
    assert balance <= cap;
    assert amount >= 0;
    double old_balance = balance; // snapshot of the initial state
    bool result;
    if (balance + amount <= cap) {
        balance += cap;
        result = true;
    } else {
        result = false;
    }
    assert (result && balance == old balance + amount) || (!result && balance == old balance)
    assert balance <= cap;
    return result;
}

当然,没人会写这样的代码,除非你用艾菲尔(这可能是个好主意),但你知道这个想法。这是一个没有所有条件的版本。

public boolean add(double amount) {
    if (balance + amount <= cap) {
        balance += cap;
        return true;
    } else {
        return false;
}

请注意LSP的原始版本("If for each object") o_1 类型 S 有一物 o_2 类型 T 以致于对于所有的程序 P 定义为 T的行为。P 时不变。o_1 取而代之的是 o_2那么 ST") 违反. 你必须定义 o_2 对每个程序都适用。选择一个上限,比如说 1000. 我就写下面的程序。

Account a = ...
if (a.add(1001)) {
    // if a = o_2, you're here
} else {
    // else you might be here.
}

这不是一个问题,因为,当然,每个人都使用LSP的弱化版本: 我们不希望beahvior是... ... 不变 子类型会有有限的利益,比如性能,想想数组列表与链接列表)),我们要把所有的 "该方案的理想特性" (见 这个问题).


4
投票

重要的是要记住LSP涵盖了语法和语义。它涵盖了 两者 该方法的编码是为了做什么。 方法被记录下来要做什么。这意味着模糊的文档会给LSP的应用带来困难。

你如何解释这个问题?

试图将钱添加到账户中。

很明显 add() 方法并不能保证将钱添加到账户中;因此,事实是 CappedAccount.add() 可能实际上没有增加资金,这似乎是可以接受的。但是,没有任何文档说明当尝试加钱失败时,应该期待什么。由于这个用例没有被记录下来,"什么都不做 "似乎是一个可以接受的行为,因此我们没有违反LSP。

为了安全起见,我会修改文档来定义失败的预期行为。add() 即明确定义后置条件。由于LSP涵盖了语法和语义两个方面,所以可以通过修改其中一个方面来修正违规行为。

© www.soinside.com 2019 - 2024. All rights reserved.