我有3个班。Account
, CappedAccount
, UserAccount
,
CappedAccount
和 UserAccount
两者都延伸 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
算不算 "加强前提条件"?
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
那么 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的弱化版本: 我们不希望beahvior是... ... 不变 子类型会有有限的利益,比如性能,想想数组列表与链接列表)),我们要把所有的 "该方案的理想特性" (见 这个问题).
重要的是要记住LSP涵盖了语法和语义。它涵盖了 两者 该方法的编码是为了做什么。和 方法被记录下来要做什么。这意味着模糊的文档会给LSP的应用带来困难。
你如何解释这个问题?
试图将钱添加到账户中。
很明显 add()
方法并不能保证将钱添加到账户中;因此,事实是 CappedAccount.add()
可能实际上没有增加资金,这似乎是可以接受的。但是,没有任何文档说明当尝试加钱失败时,应该期待什么。由于这个用例没有被记录下来,"什么都不做 "似乎是一个可以接受的行为,因此我们没有违反LSP。
为了安全起见,我会修改文档来定义失败的预期行为。add()
即明确定义后置条件。由于LSP涵盖了语法和语义两个方面,所以可以通过修改其中一个方面来修正违规行为。