这个前置条件是否违反了Liskov替换原则?

6

我有三个类,AccountCappedAccountUserAccount

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。

例如,CappedAccount中的add()是否算作“加强前置条件”?

2个回答

4

记住LSP覆盖语法和语义。 它涵盖了方法编写的内容和方法文档中规定的内容,意味着模糊的文档可能会使应用LSP变得困难。

你是如何理解这段话的?

尝试向账户添加金额。

显然,add()方法不能保证将钱添加到账户中;因此,CappedAccount.add()未能实际添加资金似乎是可以接受的。 但是,没有文档说明当尝试添加金额失败时应该期望什么。由于该用例未经记录,因此“什么也不做”似乎是可以接受的行为,因此我们没有违反LSP。

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


1
我明白你的回答与LSP的“合约”部分有关,但是LSP的规则呢?主要是你不能加强前置条件的规则。通过验证add()不会超过上限,我是否在加强前置条件呢? - Suneet Tipirneni
3
@SuneetTipirneni,你并不是在加强一个纯净的条件,而是在限制它。Account类的合同基本上是说,“如果给定的金额可以添加,那么就会被添加”。它没有指定“可以添加”的含义,因此子类可以自由地添加他们的规则。这应该在合同文档中更清晰地说明;您将在文档中发现类似于“子类可能具有关于有效值的不同条件”的措辞。 - daniu

2
TLDR;(太长不看)
if (balance + amount > cap) {
    return;
}
不是前置条件而是不变量,因此不会(自己)违反Liskov替换原则。
现在,正式回答。
真正的前置条件应该是(伪代码):
[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;
}

当然,除非你使用Eiffel(这可能是个好主意),否则没有人会写出那样的代码,但你可以理解这个想法。这里有一个没有所有条件的版本:

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

请注意,LSP原始版本中的内容为:“如果对于类型为S的每个对象o_1,都存在类型为T的对象o_2,使得对于所有基于T定义的程序P,在将o_1替换为o_2时,P的行为保持不变,则S是T的子类型”。该规则被违反了。您需要定义一个适用于每个程序的o_2。选择一个上限,比如说1000。我将编写以下程序:
Account a = ...
if (a.add(1001)) {
    // if a = o_2, you're here
} else {
    // else you might be here.
}

这不是问题,因为当然,每个人都使用了一个弱化版本的LSP:我们不希望行为保持不变(子类型可能只有有限的兴趣,例如性能,考虑数组列表和链接列表),我们希望保留所有“该程序的可取属性”(请参见此问题)。


非常好的解释!这涵盖了所有基础知识,并让我更全面地理解了LSP。谢谢! - Suneet Tipirneni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接