面向对象程序设计(Object-Oriented Programming, OOP)是软件开发中常用的一种编程思想。在OOP中,封装、继承、多态为主要特性,而继承是更加通用、也是更加基础的特性。在继承中,里氏代换原则(Liskov Substitution Principle, LSP)是非常重要的一项原则,它可使得代码更加稳定可靠。
LSP是由计算机专家芭芭拉·利斯科夫(Barbara Liskov)提出的。大致定义如下:如果S是T的子类,那么任何类型为T的对象在程序中可以被替换为类型为S的对象,而不会影响程序的正确性。也就是说,子类对象必须能够替换父类对象并且以父类对象的行为来使用,这就需要子类去保证实现了父类的所有行为,不改变已有的父类方法的前置条件、后置条件和异常返回值。
LSP的定义虽然简单,但是它所关注的方面却深刻而广泛。在应用LSP时,从编写代码到软件系统设计,甚至到软件开发管理等各个方面都要受到它的考验。下面具体介绍LSP原则对代码编写的影响。
1.避免子类破坏父类的行为
LSP的核心是确保子类能够替换父类,这就要求子类必须保证不会改变已有的父类方法的前置条件,后置条件和异常返回值。假设A是B的子类,B中有一个方法foo,那么A中也可以有foo方法,并且A的foo方法接收的参数类型和返回类型必须和B中的foo方法相同。在这个基础上,A可以对foo方法做些自己的实现。
然而,如果A对foo方法的实现方式在某些情况下和B的实现不一样,那么A就不能完全取代B。这会导致A不能替换B,也违反了LSP。在实现子类时,必须保证子类的行为不会破坏父类。如果子类需要有自己不同的行为,那么我们可以增加父类方法的参数或者返回值,以便子类可以实现自己的行为。
接下来,我们将用代码来说明,假设我们有如下的抽象汽车类AbstractCar。
```python
class AbstractCar:
def run(self):
raise NotImplementedError
```
假设我们有一种新型的汽车class NewCar(AbstractCar):,它增加了一些自己的行为。我们不希望NewCar破坏父类AbstractCar的方法,因此我们可以通过扩展接口的方式来添加新的行为。
```python
class AbstractCar:
def run(self):
raise NotImplementedError
def door_close(self):
raise NotImplementedError
def door_open(self):
raise NotImplementedError
def turn_left(self):
raise NotImplementedError
def turn_right(self):
raise NotImplementedError
class NewCar(AbstractCar):
def run(self):
print("NewCar run")
def door_close(self):
print("NewCar closed the door")
def door_open(self):
print("NewCar opened the door")
def turn_left(self):
print("NewCar turned left")
def turn_right(self):
print("NewCar turned right")
def alarm(self):
print("NewCar alarm")
```
2.子类可以增加方法
LSP要求子类不可破坏父类的行为,但是不要求子类不能增加自己的行为。对于抽象的父类,具体的子类可以在不影响原有父类方法的情况下,增加自己的新方法。这是因为父类和子类之间,不仅仅可以通过继承来实现代码重用,同时,通过组合也可以实现代码的复用。
接下来我们将通过代码来演示,增加一个MonkeyCar子类,并且增加一个猴子的方法Monkey :
```python
class MonkeyCar(AbstractCar):
def run(self):
print("MonkeyCar run")
def door_close(self):
print("MonkeyCar closed the door")
def door_open(self):
print("MonkeyCar opened the door")
def turn_left(self):
print("MonkeyCar turned left")
def turn_right(self):
print("MonkeyCar turned right")
def monkey(self):
print("Monkey come out")
```
3.使用interface定接口规范
LSP强调的是接口规范的一致性。设想以下场景:如果我们有两个汽车类,他们都是从AbstractCar类继承的,并且都有run方法。那么对于上层代码(主程序等)来说,它们是一样的,程序不需要关心具体是哪个类。这就意味着代码的耦合性降低了。但是如果新添加一个普通车CommonCar类,它也是从AbstractCar类继承的,但是却没有door_open方法,这会使得上层代码在处理普通车的时候需要加入特判,这本质上是不符合开闭原则的。因此,在OOP中,不仅可以通过继承来实现代码的扩展,同时,在引入新的类时,我们可以通过接口来规范方法的实现、命名以及其他要求。下面是具体的实现方式:
我们定义一个新的接口,来规范每一个汽车都必须具备run和run_speed两个方法
```python
from abc import ABCMeta, abstractmethod
class ICar(metaclass=ABCMeta):
@abstractmethod
def run(self):
pass
@abstractmethod
def run_speed(self, speed):
pass
```
在我们不同的汽车类中,通过继承来实现不同方式的run方法和run_speed方法。
新车的实现:
```python
class NewCar(ICar):
def run(self):
print("NewCar run")
def run_speed(self, speed):
print(f"NewCar running at {speed} speed.")
```
猴子车的实现:
```python
class MonkeyCar(ICar):
def run(self):
print("MonkeyCar run")
def run_speed(self, speed):
print(f"MonkeyCar running {speed} speed.")
def door_close(self):
print("MonkeyCar closed the door")
def door_open(self):
print("MonkeyCar opened the door")
def turn_left(self):
print("MonkeyCar turned left")
def turn_right(self):
print("MonkeyCar turned right")
def monkey(self):
print("Monkey come out")
```
这时,我们添加一个普通车的实现,让它也遵循我们ICar接口实现即可。
4.通过泛型来应用LSP
在OOP中,泛型是一种强大的方式,可以在编写代码时,避免很多冗余的类型检查操作。Python 3.x对泛型添加了很好的支持,可以很方便的应用LSP。我们在ICar中引入TypeVar,类似于Java中的,然后我们就可以通过TypeVar来实现泛型中的范型参数。
```python
from abc import ABCMeta, abstractmethod
from typing import TypeVar
T = TypeVar("T", bound="ICar")
class ICar(metaclass=ABCMeta):
@abstractmethod
def run(self) -> None:
pass
@abstractmethod
def run_speed(self, speed: float) -> None:
pass
@abstractmethod
def same(self, x: T) -> bool:
pass
```
代码实现:
```python
class CommonCar(ICar):
def run(self):
print("CommonCar run")
def run_speed(self, speed):
print(f"CommonCar running {speed} speed.")
def same(self, x: T) -> bool:
return isinstance(x, CommonCar)
```
这其中最重要的是我们定义了一个泛型T去继承ICar,并且实现了一个same的方法,判断变量的类型参数是否匹配。这样,在应用泛型参数时,我们就可以很容易的应用LSP原则实现代码的重用。
总结:
继承是OOP中的重要特性,也是代码复用的核心。同时,代码的稳定可靠性是在开发过程中必须保证的一个重要性质。LSP原则作为一个重要的编码规约,帮助开发人员在编写代码时遵循良好的设计原则,有效地提高了代码稳定可靠性。在遵循LSP原则的基础上,我们可以更加高效地编写出复用性更好,扩展性更强的代码。