Liskov Substitution Principle in Java – Java中的利斯科夫置换原理

最后修改: 2020年 7月 25日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The SOLID design principles were introduced by Robert C. Martin in his 2000 paper, Design Principles and Design Patterns. SOLID design principles help us create more maintainable, understandable, and flexible software.

SOLID设计原则是由Robert C. Martin在其2000年的论文中提出的,设计原则和设计模式。SOLID设计原则帮助我们创建更可维护、更易理解和更灵活的软件。

In this article, we’ll discuss the Liskov Substitution Principle, which is the “L” in the acronym.

在这篇文章中,我们将讨论利斯科夫替代原理,也就是缩写中的 “L”。

2. The Open/Closed Principle

2.开放/封闭原则

To understand the Liskov Substitution Principle, we must first understand the Open/Closed Principle (the “O” from SOLID).

要理解利斯科夫替代原理,我们必须首先理解开放/封闭原理(SOLID的 “O”)。

The goal of the Open/Closed principle encourages us to design our software so we add new features only by adding new code. When this is possible, we have loosely coupled, and thus easily maintainable applications.

开放/封闭原则的目标鼓励我们设计我们的软件,以便我们只通过添加新代码来增加新功能。如果能做到这一点,我们就有了松散的耦合,从而使应用程序易于维护。

3. An Example Use Case

3.一个用例

Let’s look at a Banking Application example to understand the Open/Closed Principle some more.

让我们看看一个银行应用的例子,以进一步了解开放/关闭原则。

3.1. Without the Open/Closed Principle

3.1.没有开放/封闭原则

Our banking application supports two account types – “current” and “savings”. These are represented by the classes CurrentAccount and SavingsAccount respectively.

我们的银行应用程序支持两种账户类型 – “活期 “和 “储蓄”。它们分别由CurrentAccountSavingsAccount类代表。

The BankingAppWithdrawalService serves the withdrawal functionality to its users:

BankingAppWithdrawalService为其用户提供提款功能。

Unfortunately, there is a problem with extending this design. The BankingAppWithdrawalService is aware of the two concrete implementations of account. Therefore, the BankingAppWithdrawalService would need to be changed every time a new account type is introduced.

不幸的是,在扩展这个设计时有一个问题。BankingAppWithdrawalService知道account的两个具体实现。因此,BankingAppWithdrawalService将需要在每次引入新的帐户类型时进行更改。

3.2. Using the Open/Closed Principle to Make the Code Extensible

3.2.使用开放/封闭原则来使代码可扩展

Let’s redesign the solution to comply with the Open/Closed principle. We’ll close BankingAppWithdrawalService from modification when new account types are needed, by using an Account base class instead:

让我们重新设计解决方案以符合开放/关闭原则。当需要新的账户类型时,我们将关闭BankingAppWithdrawalService的修改,而使用Account基类。

Here, we introduced a new abstract Account class that CurrentAccount and SavingsAccount extend.

在这里,我们引入了一个新的抽象的Account类,CurrentAccountSavingsAccount扩展了这个类。

The BankingAppWithdrawalService no longer depends on concrete account classes. Because it now depends only on the abstract class, it need not be changed when a new account type is introduced.

BankingAppWithdrawalService不再依赖于具体的账户类。因为它现在只依赖于抽象类,所以当引入新的账户类型时,它不需要被改变。

Consequently, the BankingAppWithdrawalService is open for the extension with new account types, but closed for modification, in that the new types don’t require it to change in order to integrate.

因此,BankingAppWithdrawalService对扩展新的账户类型是开放的,但对修改封闭的,因为新的类型不需要它改变以实现整合。

3.3. Java Code

3.3 Java代码

Let’s look at this example in Java. To begin with, let’s define the Account class:

让我们在Java中看一下这个例子。首先,让我们定义一下Account类。

public abstract class Account {
    protected abstract void deposit(BigDecimal amount);

    /**
     * Reduces the balance of the account by the specified amount
     * provided given amount > 0 and account meets minimum available
     * balance criteria.
     *
     * @param amount
     */
    protected abstract void withdraw(BigDecimal amount);
}

And, let’s define the BankingAppWithdrawalService:

然后,让我们定义BankingAppWithdrawalService

public class BankingAppWithdrawalService {
    private Account account;

    public BankingAppWithdrawalService(Account account) {
        this.account = account;
    }

    public void withdraw(BigDecimal amount) {
        account.withdraw(amount);
    }
}

Now, let’s look at how, in this design, a new account type might violate the Liskov Substitution Principle.

现在,让我们来看看,在这种设计中,新的账户类型如何可能违反利斯科夫替代原则。

3.4. A New Account Type

3.4.一个新的账户类型

The bank now wants to offer a high interest-earning fixed-term deposit account to its customers.

该银行现在希望向客户提供一个高利息的定期存款账户。

To support this, let’s introduce a new FixedTermDepositAccount class. A fixed-term deposit account in the real world “is a” type of account. This implies inheritance in our object-oriented design.

为了支持这一点,让我们引入一个新的FixedTermDepositAccount类。在现实世界中,一个定期存款账户 “是一个 “账户类型。这意味着在我们面向对象的设计中要有继承性。

So, let’s make FixedTermDepositAccount a subclass of Account:

所以,让我们把FixedTermDepositAccount作为Account的子类。

public class FixedTermDepositAccount extends Account {
    // Overridden methods...
}

So far, so good. However, the bank doesn’t want to allow withdrawals for the fixed-term deposit accounts.

到目前为止,一切都很好。然而,银行不想让定期存款账户的提款。

This means that the new FixedTermDepositAccount class can’t meaningfully provide the withdraw method that Account defines. One common workaround for this is to make FixedTermDepositAccount throw an UnsupportedOperationException in the method it cannot fulfill:

这意味着新的FixedTermDepositAccount类不能有意义地提供Account定义的withdraw方法。一个常见的解决方法是让FixedTermDepositAccount在它不能实现的方法中抛出一个UnsupportedOperationException

public class FixedTermDepositAccount extends Account {
    @Override
    protected void deposit(BigDecimal amount) {
        // Deposit into this account
    }

    @Override
    protected void withdraw(BigDecimal amount) {
        throw new UnsupportedOperationException("Withdrawals are not supported by FixedTermDepositAccount!!");
    }
}

3.5. Testing Using the New Account Type

3.5.使用新账户类型进行测试

While the new class works fine, let’s try to use it with the BankingAppWithdrawalService:

虽然新的类工作正常,但让我们试着将其与BankingAppWithdrawalService一起使用。

Account myFixedTermDepositAccount = new FixedTermDepositAccount();
myFixedTermDepositAccount.deposit(new BigDecimal(1000.00));

BankingAppWithdrawalService withdrawalService = new BankingAppWithdrawalService(myFixedTermDepositAccount);
withdrawalService.withdraw(new BigDecimal(100.00));

Unsurprisingly, the banking application crashes with the error:

不出所料,银行应用程序崩溃时出现了错误。

Withdrawals are not supported by FixedTermDepositAccount!!

There’s clearly something wrong with this design if a valid combination of objects results in an error.

如果一个有效的对象组合导致了一个错误,那么这个设计显然是有问题的。

3.6. What Went Wrong?

3.6.什么地方出错了?

The BankingAppWithdrawalService is a client of the Account class. It expects that both Account and its subtypes guarantee the behavior that the Account class has specified for its withdraw method:

BankingAppWithdrawalServiceAccount类的一个客户端。它希望Account及其子类型都能保证Account类为其withdraw方法指定的行为。

/**
 * Reduces the account balance by the specified amount
 * provided given amount > 0 and account meets minimum available
 * balance criteria.
 *
 * @param amount
 */
protected abstract void withdraw(BigDecimal amount);

However, by not supporting the withdraw method, the FixedTermDepositAccount violates this method specification. Therefore, we cannot reliably substitute FixedTermDepositAccount for Account.

然而,由于不支持withdraw方法,FixedTermDepositAccount违反了这个方法规范因此,我们不能可靠地用FixedTermDepositAccount替代Account

In other words, the FixedTermDepositAccount has violated the Liskov Substitution Principle.

换句话说,FixedTermDepositAccount已经违反了Liskov替代原则。

3.7. Can’t We Handle the Error in BankingAppWithdrawalService?

3.7.我们不能处理BankingAppWithdrawalService中的错误吗?

We could amend the design so that the client of Account‘s withdraw method has to be aware of a possible error in calling it. However, this would mean that clients have to have special knowledge of unexpected subtype behavior. This starts to break the Open/Closed principle.

我们可以修改设计,使Accountwithdraw方法的客户必须知道在调用该方法时可能出现的错误。然而,这将意味着客户必须对意外的子类型行为有特别的了解。这就开始打破了开放/封闭原则。

In other words, for the Open/Closed Principle to work well, all subtypes must be substitutable for their supertype without ever having to modify the client code. Adhering to the Liskov Substitution Principle ensures this substitutability.

换句话说,为了使 “开放/封闭原则 “能够很好地发挥作用,所有的子类型必须可以替代它们的超类型,而不需要修改客户端代码。遵循Liskov替代原则可以确保这种可替代性。

Let’s now look at the Liskov Substitution Principle in detail.

现在让我们详细了解一下利斯科夫替代原理。

4. The Liskov Substitution Principle

4.利斯科夫替代原理

4.1. Definition

4.1 定义

Robert C. Martin summarizes it:

Robert C. Martin总结道。

Subtypes must be substitutable for their base types.

子类型必须可以替代其基本类型。

Barbara Liskov, defining it in 1988, provided a more mathematical definition:

Barbara Liskov在1988年对其进行了定义,提供了一个更加数学化的定义。

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

如果对于S类型的每个对象o1,都有一个T类型的对象o2,并且对于以T定义的所有程序P,当o1被替换为o2时,P的行为没有变化,那么S就是T的一个子类型。

Let’s understand these definitions a bit more.

让我们再来了解一下这些定义。

4.2. When Is a Subtype Substitutable for Its Supertype?

4.2.子类型何时可替代其超类型?

A subtype doesn’t automatically become substitutable for its supertype. To be substitutable, the subtype must behave like its supertype.

一个子类型不会自动成为其超类型的可替代物。要成为可替换的,子类型必须表现得像它的超类型

An object’s behavior is the contract that its clients can rely on. The behavior is specified by the public methods, any constraints placed on their inputs, any state changes that the object goes through, and the side effects from the execution of methods.

一个对象的行为是其客户可以依赖的契约。该行为是由公共方法、对其输入的任何约束、对象经历的任何状态变化以及方法执行的副作用所指定。

Subtyping in Java requires the base class’s properties and methods are available in the subclass.

Java中的子类型要求基类的属性和方法在子类中可用。

However, behavioral subtyping means that not only does a subtype provide all of the methods in the supertype, but it must adhere to the behavioral specification of the supertype. This ensures that any assumptions made by the clients about the supertype behavior are met by the subtype.

然而,行为子类型化意味着子类型不仅要提供超类型中的所有方法,而且必须遵守超类型的行为规范。这确保了子类型能够满足客户对超类型行为的任何假设。

This is the additional constraint that the Liskov Substitution Principle brings to object-oriented design.

这就是Liskov替代原则给面向对象设计带来的额外约束。

Let’s now refactor our banking application to address the problems we encountered earlier.

现在让我们重构我们的银行应用程序,以解决我们之前遇到的问题。

5. Refactoring

5.重构

To fix the problems we found in the banking example, let’s start by understanding the root cause.

为了解决我们在银行业例子中发现的问题,让我们从了解根本原因开始。

5.1. The Root Cause

5.1.根本原因

In the example, our FixedTermDepositAccount was not a behavioral subtype of Account.

在这个例子中,我们的FixedTermDepositAccount不是Account的一个行为子类型。

The design of Account incorrectly assumed that all Account types allow withdrawals. Consequently, all subtypes of Account, including FixedTermDepositAccount which doesn’t support withdrawals, inherited the withdraw method.

Account的设计错误地假定所有Account类型都允许提款。因此,Account的所有子类型,包括不支持提款的FixedTermDepositAccount,继承了withdraw方法。

Though we could work around this by extending the contract of Account, there are alternative solutions.

虽然我们可以通过延长Account的合同来解决这个问题,但也有其他的解决方案。

5.2. Revised Class Diagram

5.2.修订后的类图

Let’s design our account hierarchy differently:

让我们以不同的方式来设计我们的账户层次结构。

Because all accounts do not support withdrawals, we moved the withdraw method from the Account class to a new abstract subclass WithdrawableAccount. Both CurrentAccount and SavingsAccount allow withdrawals. So they’ve now been made subclasses of the new WithdrawableAccount.

因为所有账户都不支持取款,我们把withdraw方法从Account类移到一个新的抽象子类WithdrawableAccountCurrentAccountSavingsAccount都允许提款。所以它们现在已经成为新的WithdrawableAccount的子类。

This means BankingAppWithdrawalService can trust the right type of account to provide the withdraw function.

这意味着BankingAppWithdrawalService可以信任正确的账户类型来提供withdraw功能。

5.3. Refactored BankingAppWithdrawalService

5.3.重构了BankingAppWithdrawalService

BankingAppWithdrawalService now needs to use the WithdrawableAccount:

BankingAppWithdrawalService现在需要使用WithdrawableAccount:。

public class BankingAppWithdrawalService {
    private WithdrawableAccount withdrawableAccount;

    public BankingAppWithdrawalService(WithdrawableAccount withdrawableAccount) {
        this.withdrawableAccount = withdrawableAccount;
    }

    public void withdraw(BigDecimal amount) {
        withdrawableAccount.withdraw(amount);
    }
}

As for FixedTermDepositAccount, we retain Account as its parent class. Consequently, it inherits only the deposit behavior that it can reliably fulfill and no longer inherits the withdraw method that it doesn’t want. This new design avoids the issues we saw earlier.

至于FixedTermDepositAccount,我们保留Account作为其父类。因此,它只继承了它能可靠完成的存款行为,而不再继承它不想要的提款方法。这个新的设计避免了我们之前看到的问题。

6. Rules

6.规则

Let’s now look at some rules/techniques concerning method signatures, invariants, preconditions, and postconditions that we can follow and use to ensure we create well-behaved subtypes.

现在让我们看看一些关于方法签名、不变量、前条件和后条件的规则/技术,我们可以遵循和使用这些规则/技术来确保我们创建了良好的子类型。

In their book Program Development in Java: Abstraction, Specification, and Object-Oriented Design, Barbara Liskov and John Guttag grouped these rules into three categories – the signature rule, the properties rule, and the methods rule.

在他们的书Program Development in Java:Abstraction, Specification, and Object-Oriented Design中,Barbara Liskov和John Guttag将这些规则归为三类–签名规则、属性规则和方法规则。

Some of these practices are already enforced by Java’s overriding rules.

其中一些做法已经被Java的凌驾性规则所强制执行。

We should note some terminology here. A wide type is more general – Object for instance could mean ANY Java object and is wider than, say, CharSequence, where String is very specific and therefore narrower.

我们应该注意这里的一些术语。一个宽泛的类型更为普遍–例如,Object可以指任何Java对象,比CharSequence更宽泛,而String则非常具体,因此更窄。

6.1. Signature Rule – Method Argument Types

6.1.签名规则–方法参数类型

This rule states that the overridden subtype method argument types can be identical or wider than the supertype method argument types.

这条规则规定,被覆盖的子类型方法参数类型可以与超类型方法参数类型相同或更宽

Java’s method overriding rules support this rule by enforcing that the overridden method argument types match exactly with the supertype method.

Java的方法覆盖规则通过强制要求被覆盖的方法参数类型与超类型方法完全匹配来支持这一规则。

6.2. Signature Rule – Return Types

6.2.签名规则–返回类型

The return type of the overridden subtype method can be narrower than the return type of the supertype method. This is called covariance of the return types. Covariance indicates when a subtype is accepted in place of a supertype. Java supports the covariance of return types. Let’s look at an example:

被覆盖的子类型方法的返回类型可以比超类型方法的返回类型更窄。这被称为返回类型的协变性。共变性表示当一个子类型被接受以代替一个超类型时。Java支持返回类型的协变性。让我们看一个例子。

public abstract class Foo {
    public abstract Number generateNumber();    
    // Other Methods
}

The generateNumber method in Foo has return type as Number. Let’s now override this method by returning a narrower type of Integer:

Foo中的generateNumber方法的返回类型为Number。现在让我们通过返回一个更窄的类型Integer来重写这个方法。

public class Bar extends Foo {
    @Override
    public Integer generateNumber() {
        return new Integer(10);
    }
    // Other Methods
}

Because Integer IS-A Number, a client code that expects Number can replace Foo with Bar without any problems.

因为Integer是一个Number,期待Number的客户端代码可以将Foo替换为Bar而不会有任何问题。

On the other hand, if the overridden method in Bar were to return a wider type than Number, e.g. Object, that might include any subtype of Object e.g. a Truck. Any client code that relied on the return type of Number could not handle a Truck!

另一方面,如果Bar中的重载方法要返回比Number更广泛的类型,例如Object,这可能包括Object的任何子类型,例如Truck。任何依赖Number的返回类型的客户端代码都无法处理Truck!

Fortunately, Java’s method overriding rules prevent an override method returning a wider type.

幸运的是,Java的方法重写规则可以防止重写方法返回更广泛的类型。

6.3. Signature Rule – Exceptions

6.3.签名规则–例外情况

The subtype method can throw fewer or narrower (but not any additional or broader) exceptions than the supertype method.

子类型方法可以抛出比超级类型方法更少或更窄的(但不是任何额外的或更广泛的)异常

This is understandable because when the client code substitutes a subtype, it can handle the method throwing fewer exceptions than the supertype method. However, if the subtype’s method throws new or broader checked exceptions, it would break the client code.

这是可以理解的,因为当客户端代码替换了一个子类型时,它可以处理抛出比超类型方法更少的异常。然而,如果子类型的方法抛出新的或更广泛的检查异常,就会破坏客户端代码。

Java’s method overriding rules already enforce this rule for checked exceptions. However, overriding methods in Java CAN THROW any RuntimeException regardless of whether the overridden method declares the exception.

Java的方法覆盖规则已经对检查的异常执行了这个规则。然而,Java中的覆盖方法可以抛出任何RuntimeException,无论被覆盖的方法是否声明了该异常。

6.4. Properties Rule – Class Invariants

6.4.属性规则–类不变量

A class invariant is an assertion concerning object properties that must be true for all valid states of the object.

类不变性是一个关于对象属性的断言,对于对象的所有有效状态来说必须是真的。

Let’s look at an example:

我们来看看一个例子。

public abstract class Car {
    protected int limit;

    // invariant: speed < limit;
    protected int speed;

    // postcondition: speed < limit
    protected abstract void accelerate();

    // Other methods...
}

The Car class specifies a class invariant that speed must always be below the limit. The invariants rule states that all subtype methods (inherited and new) must maintain or strengthen the supertype’s class invariants.

汽车类规定了一个类不变式,即速度必须总是低于极限。不变量规则指出,所有子类型的方法(继承的和新的)必须保持或加强超类型的类不变量

Let’s define a subclass of Car that preserves the class invariant:

让我们定义一个Car的子类,它保留了类的不变性。

public class HybridCar extends Car {
    // invariant: charge >= 0;
    private int charge;

      @Override
    // postcondition: speed < limit
    protected void accelerate() {
        // Accelerate HybridCar ensuring speed < limit
    }

    // Other methods...
}

In this example, the invariant in Car is preserved by the overridden accelerate method in HybridCar. The HybridCar additionally defines its own class invariant charge >= 0, and this is perfectly fine.

在这个例子中,Car中的不变量被HybridCar中被重载的accelerate方法保留了。HybridCar另外定义了它自己的类不变式charge >= 0,这是很好的。

Conversely, if the class invariant is not preserved by the subtype, it breaks any client code that relies on the supertype.

相反,如果类的不变性没有被子类型所保留,它就会破坏任何依赖超类型的客户代码。

6.5. Properties Rule – History Constraint

6.5.属性规则–历史约束

The history constraint states that the subclass methods (inherited or new) shouldn’t allow state changes that the base class didn’t allow.

历史约束规定,子类方法(继承或新建)不应该允许基类不允许的状态变化

Let’s look at an example:

我们来看看一个例子。

public abstract class Car {

    // Allowed to be set once at the time of creation.
    // Value can only increment thereafter.
    // Value cannot be reset.
    protected int mileage;

    public Car(int mileage) {
        this.mileage = mileage;
    }

    // Other properties and methods...

}

The Car class specifies a constraint on the mileage property. The mileage property can be set only once at the time of creation and cannot be reset thereafter.

Car类对mileage属性规定了一个约束。mileage属性只能在创建时被设置一次,此后不能被重置。

Let’s now define a ToyCar that extends Car:

现在让我们定义一个ToyCar,它扩展了Car:

public class ToyCar extends Car {
    public void reset() {
        mileage = 0;
    }

    // Other properties and methods
}

The ToyCar has an extra method reset that resets the mileage property. In doing so, the ToyCar ignored the constraint imposed by its parent on the mileage property. This breaks any client code that relies on the constraint. So, ToyCar isn’t substitutable for Car.

ToyCar有一个额外的方法reset来重置mileage属性。在这样做的时候,ToyCar忽略了其父辈对mileage属性所施加的约束。这破坏了任何依赖该约束的客户端代码。所以,ToyCar不能替代Car

Similarly, if the base class has an immutable property, the subclass should not permit this property to be modified. This is why immutable classes should be final.

同样地,如果基类有一个不可变的属性,子类不应该允许修改这个属性。这就是为什么不可变的类应该是final

6.6. Methods Rule – Preconditions

6.6.方法规则 – 前提条件

A precondition should be satisfied before a method can be executed. Let’s look at an example of a precondition concerning parameter values:

一个前提条件应该在一个方法被执行之前得到满足。让我们看一个关于参数值的前提条件的例子。

public class Foo {

    // precondition: 0 < num <= 5
    public void doStuff(int num) {
        if (num <= 0 || num > 5) {
            throw new IllegalArgumentException("Input out of range 1-5");
        }
        // some logic here...
    }
}

Here, the precondition for the doStuff method states that the num parameter value must be between 1 and 5. We have enforced this precondition with a range check inside the method. A subtype can weaken (but not strengthen) the precondition for a method it overrides. When a subtype weakens the precondition, it relaxes the constraints imposed by the supertype method.

这里,doStuff方法的前提条件是num参数值必须在1到5之间。我们通过方法内部的范围检查来强制执行这个前提条件。一个子类型可以削弱(但不能加强)其覆盖的方法的前提条件。当一个子类型弱化前提条件时,它放松了由超类型方法施加的约束。

Let’s now override the doStuff method with a weakened precondition:

现在让我们用一个弱化的前提条件来覆盖doStuff方法。

public class Bar extends Foo {

    @Override
    // precondition: 0 < num <= 10
    public void doStuff(int num) {
        if (num <= 0 || num > 10) {
            throw new IllegalArgumentException("Input out of range 1-10");
        }
        // some logic here...
    }
}

Here, the precondition is weakened in the overridden doStuff method to 0 < num <= 10, allowing a wider range of values for num. All values of num that are valid for Foo.doStuff are valid for Bar.doStuff as well. Consequently, a client of Foo.doStuff doesn’t notice a difference when it replaces Foo with Bar.

这里,前提条件在重载的doStuff方法中被弱化为0 < num <= 10,允许num有更大的取值范围。所有对Foo.doStuff有效的num值,对Bar.doStuff也有效。因此,Foo.doStuff的客户端在用Bar替换Foo时不会注意到任何不同。

Conversely, when a subtype strengthens the precondition (e.g. 0 < num <= 3 in our example), it applies more stringent restrictions than the supertype. For example, values 4 & 5 for num are valid for Foo.doStuff, but are no longer valid for Bar.doStuff.

相反,当一个子类型加强了前提条件(例如,在我们的例子中,0 < num <= 3),它应用了比超类型更严格的限制。例如,num的值4和5对Foo.doStuff有效,但对Bar.doStuff不再有效。

This would break the client code that does not expect this new tighter constraint.

这将破坏不期望这个新的更严格约束的客户代码。

6.7. Methods Rule – Postconditions

6.7.方法规则 – 后置条件

A postcondition is a condition that should be met after a method is executed.

postcondition是在执行方法后应该满足的条件。

Let’s look at an example:

我们来看看一个例子。

public abstract class Car {

    protected int speed;

    // postcondition: speed must reduce
    protected abstract void brake();

    // Other methods...
}

Here, the brake method of Car specifies a postcondition that the Car‘s speed must reduce at the end of the method execution.  The subtype can strengthen (but not weaken) the postcondition for a method it overrides. When a subtype strengthens the postcondition, it provides more than the supertype method.

这里,Carbrake方法指定了一个后置条件,即Carspeed在方法执行结束时必须降低。 子类型可以加强(但不能削弱)其覆盖的方法的后置条件。当一个子类型加强了后置条件时,它提供了比超类型方法更多的后置条件。

Now, let’s define a derived class of Car that strengthens this precondition:

现在,让我们定义一个Car的派生类,加强这个前提条件。

public class HybridCar extends Car {

   // Some properties and other methods...

    @Override
    // postcondition: speed must reduce
    // postcondition: charge must increase
    protected void brake() {
        // Apply HybridCar brake
    }
}

The overridden brake method in HybridCar strengthens the postcondition by additionally ensuring that the charge is increased as well. Consequently, any client code relying on the postcondition of the brake method in the Car class notices no difference when it substitutes HybridCar for Car.

HybridCar中被重载的brake方法通过额外确保charge也被增加而加强了后置条件。因此,任何依赖于Car类中brake方法的后置条件的客户代码在用HybridCar替换Car时不会注意到任何不同。

Conversely, if HybridCar were to weaken the postcondition of the overridden brake method, it would no longer guarantee that the speed would be reduced. This might break client code given a HybridCar as a substitute for Car.

相反,如果HybridCar削弱了被覆盖的brake方法的后置条件,它将不再保证speed会被降低。这可能会破坏客户的代码,因为客户使用HybridCar作为Car的替代。

7. Code Smells

7.代码气味

How can we spot a subtype that is not substitutable for its supertype in the real world?

我们怎样才能发现一个在现实世界中无法替代其超类型的子类型?

Let’s look at some common code smells that are signs of a violation of the Liskov Substitution Principle.

让我们看看一些常见的代码气味,它们是违反利斯科夫替代原则的标志。

7.1. A Subtype Throws an Exception for a Behavior It Can’t Fulfill

7.1.一个子类型为它不能实现的行为抛出一个异常

We have seen an example of this in our banking application example earlier on.

我们在前面的银行应用实例中已经看到了这样的例子。

Prior to the refactoring, the Account class had an extra method withdraw that its subclass FixedTermDepositAccount didn’t want. The FixedTermDepositAccount class worked around this by throwing the UnsupportedOperationException for the withdraw method. However, this was just a hack to cover up a weakness in the modeling of the inheritance hierarchy.

在重构之前,Account类有一个额外的方法withdraw,它的子类FixedTermDepositAccount不想要这个方法。FixedTermDepositAccount类通过为withdraw方法抛出UnsupportedOperationException来解决这个问题。然而,这只是一个黑客,用来掩盖继承层次结构建模中的一个弱点。

7.2. A Subtype Provides No Implementation for a Behavior It Can’t Fulfill

7.2.一个子类型不为它不能实现的行为提供实现

This is a variation of the above code smell. The subtype cannot fulfill a behavior and so it does nothing in the overridden method.

这是上述代码气味的一个变种。子类型不能完成一个行为,所以它在重载方法中什么都不做。

Here’s an example. Let’s define a FileSystem interface:

这里有一个例子。我们来定义一个文件系统接口。

public interface FileSystem {
    File[] listFiles(String path);

    void deleteFile(String path) throws IOException;
}

Let’s define a ReadOnlyFileSystem that implements FileSystem:

让我们定义一个只读文件系统,实现文件系统:

public class ReadOnlyFileSystem implements FileSystem {
    public File[] listFiles(String path) {
        // code to list files
        return new File[0];
    }

    public void deleteFile(String path) throws IOException {
        // Do nothing.
        // deleteFile operation is not supported on a read-only file system
    }
}

Here, the ReadOnlyFileSystem doesn’t support the deleteFile operation and so doesn’t provide an implementation.

这里,只读文件系统不支持删除文件操作,所以没有提供实现。

7.3. The Client Knows About Subtypes

7.3.客户端知道子类型的情况

If the client code needs to use instanceof or downcasting, then the chances are that both the Open/Closed Principle and the Liskov Substitution Principle have been violated.

如果客户端代码需要使用instanceof或下转换,那么就有可能违反了Open/Closed原则和Liskov Substitution原则。

Let’s illustrate this using a FilePurgingJob:

让我们用一个FilePurgingJob来说明这个问题。

public class FilePurgingJob {
    private FileSystem fileSystem;

    public FilePurgingJob(FileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void purgeOldestFile(String path) {
        if (!(fileSystem instanceof ReadOnlyFileSystem)) {
            // code to detect oldest file
            fileSystem.deleteFile(path);
        }
    }
}

Because the FileSystem model is fundamentally incompatible with read-only file systems, the ReadOnlyFileSystem inherits a deleteFile method it can’t support. This example code uses an instanceof check to do special work based on a subtype implementation.

由于 FileSystem 模型从根本上与只读文件系统不兼容,ReadOnlyFileSystem 继承了它无法支持的 deleteFile 方法。该示例代码使用instanceof检查来进行基于子类型实现的特殊工作。

7.4. A Subtype Method Always Returns the Same Value

7.4.一个子类型方法总是返回相同的值

This is a far more subtle violation than the others and is harder to spot. In this example, ToyCar always returns a fixed value for the remainingFuel property:

这是一个比其他更微妙的违规行为,更难发现。在这个例子中,ToyCar总是为remainingFuel属性返回一个固定值。

public class ToyCar extends Car {

    @Override
    protected int getRemainingFuel() {
        return 0;
    }
}

It depends on the interface, and what the value means, but generally hardcoding what should be a changeable state value of an object is a sign that the subclass is not fulfilling the whole of its supertype and is not truly substitutable for it.

这取决于接口,以及值的含义,但一般来说,硬编码应该是一个对象的可改变的状态值,这表明子类没有履行其超类型的全部内容,也不是真正可替代的。

8. Conclusion

8.结语

In this article, we looked at the Liskov Substitution SOLID design principle.

在这篇文章中,我们看了利斯科夫替代SOLID设计原则。

The Liskov Substitution Principle helps us model good inheritance hierarchies. It helps us prevent model hierarchies that don’t conform to the Open/Closed principle.

利斯科夫替代原则帮助我们建立良好的继承层次结构模型。它帮助我们防止不符合开放/封闭原则的模型层次。

Any inheritance model that adheres to the Liskov Substitution Principle will implicitly follow the Open/Closed principle.

任何坚持利斯科夫替代原则的继承模型都会隐含地遵循开放/封闭原则。

To begin with, we looked at a use case that attempts to follow the Open/Closed principle but violates the Liskov Substitution Principle. Next, we looked at the definition of the Liskov Substitution Principle, the notion of behavioral subtyping, and the rules that subtypes must follow.

首先,我们看了一个试图遵循开放/封闭原则但违反利斯科夫替代原则的用例。接下来,我们看了Liskov替代原则的定义,行为子类型的概念,以及子类型必须遵循的规则。

Finally, we looked at some common code smells that can help us detect violations in our existing code.

最后,我们看了一些常见的代码气味,这些气味可以帮助我们发现现有代码中的违规行为。

As always, the example code from this article is available over on GitHub.

一如既往,本文的示例代码可在GitHub上获得。