我有3个类,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,我如何设计它以符合LSP。

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

最佳答案

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 !

子类型的行为应类似于其父类型(请参见下文)。

“加强”前提的唯一方法是加强不变式。因为在每个方法调用之前和之后,不变性均应为true。增强的不变式不会违反LSP(自己),因为在方法调用之前就免费提供了不变式:初始化时为真,因此在第一个方法调用之前为真。因为它是不变的,所以在第一个方法调用之后它是正确的。在下一个方法调用之前,逐步操作始终是正确的(这是一个数学归纳法...)。
class CappedAccount extends Account {
    [invariant] balance <= cap
}

在方法调用之前和之后,不变量应为true:
@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的原始版本(“对于每个类型为o_1的对象S,都有一个对象类型为o_2T,这样,对于用P定义的所有程序T,当用P代替o_1时,o_2的行为不会改变。 S,则To_2的子类型“)被违反。您必须定义适用于每个程序的1000。选择一个上限,比如ojit_code。我将编写以下程序:
Account a = ...
if (a.add(1001)) {
    // if a = o_2, you're here
} else {
    // else you might be here.
}

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

10-07 13:04