在代码中,为什么说"正方形是矩形"是个谎言?里氏替换原则(LSP)带来的5个惊人启示

在代码中,为什么说"正方形是矩形"是个谎言?里氏替换原则(LSP)带来的5个惊人启示

👤 CodeMaster📅 2024-01-15🏷️ 软件设计
#设计原则#SOLID#面向对象#软件架构

🎥 视频讲解

🎧音频讲解

📊

PPT 幻灯片

点击按钮查看配套幻灯片

在代码中,为什么说"正方形是矩形"是个谎言?

引言

在数学世界中,正方形确实是矩形的一个特例。但在代码世界中,这个看似合理的继承关系却可能导致灾难性的后果。这就是**里氏替换原则(Liskov Substitution Principle, LSP)**要告诉我们的重要真理。

第一章:什么是里氏替换原则?

里氏替换原则(LSP)是SOLID设计原则之一,由Barbara Liskov在1987年提出。其核心思想是:

子类对象必须能够替换掉所有父类对象被使用的地方,而不会产生错误或异常。

1.1 官方定义

如果对每个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。

1.2 通俗解释

简单来说就是:子类必须完全兼容父类的行为。用户在使用父类时不应该需要知道具体的子类实现。

第二章:正方形与矩形的悖论

让我们通过经典的"正方形-矩形"问题来理解LSP。

2.1 看似合理的设计

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    def set_width(self, width):
        self._width = width
    
    def set_height(self, height):
        self._height = height
    
    def get_area(self):
        return self._width * self._height

class Square(Rectangle):
    def set_width(self, width):
        self._width = width
        self._height = width  # 保持正方形特性
    
    def set_height(self, height):
        self._width = height
        self._height = height  # 保持正方形特性

2.2 问题出现了

def test_rectangle(rect):
    rect.set_width(5)
    rect.set_height(4)
    expected_area = 5 * 4  # 期望面积是20
    actual_area = rect.get_area()
    
    assert expected_area == actual_area, f"Expected {expected_area}, got {actual_area}"

# 测试矩形 - 通过
rectangle = Rectangle(0, 0)
test_rectangle(rectangle)  # ✅ 通过

# 测试正方形 - 失败!
square = Square(0, 0)
test_rectangle(square)  # ❌ 失败!实际面积是16而不是20

2.3 为什么会失败?

因为Square违反了LSP

  • Rectangle的行为约定是:set_widthset_height独立工作
  • Square改变了这个约定:设置宽度会同时改变高度
  • 当Square替换Rectangle时,程序行为发生了意外变化

第三章:LSP的5个惊人启示

启示1:继承不是"是一个"关系

错误思维:正方形是矩形,所以Square应该继承Rectangle
正确思维:子类必须能够无缝替换父类,而不仅仅是概念上的"是一个"

启示2:契约比类型更重要

父类定义了一个"契约"(行为约定),子类必须遵守这个契约。违反契约就违反了LSP。

启示3:前置条件不能加强

class Bird:
    def fly(self, altitude):
        if altitude < 0:
            raise ValueError("高度不能为负")
        # 飞行逻辑

class Penguin(Bird):  # ❌ 违反LSP
    def fly(self, altitude):
        if altitude < 0 or altitude > 0:  # 加强了前置条件
            raise ValueError("企鹅不能飞")

启示4:后置条件不能削弱

class FileWriter:
    def write(self, data):
        # 保证:数据一定会写入磁盘
        self.save_to_disk(data)

class CachedFileWriter(FileWriter):  # ❌ 违反LSP
    def write(self, data):
        # 削弱了后置条件:数据可能只在缓存中
        self.cache.append(data)

启示5:不变量必须保持

父类的不变量(invariants)在子类中也必须成立。

第四章:如何正确设计?

4.1 方案一:组合优于继承

class Shape:
    def get_area(self):
        raise NotImplementedError

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def get_area(self):
        return self.width * self.height

class Square(Shape):  # 不继承Rectangle
    def __init__(self, side):
        self.side = side
    
    def get_area(self):
        return self.side * self.side

4.2 方案二:不可变对象

class ImmutableRectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    def with_width(self, width):
        return ImmutableRectangle(width, self._height)
    
    def with_height(self, height):
        return ImmutableRectangle(self._width, height)
    
    def get_area(self):
        return self._width * self._height

第五章:实践中的LSP

5.1 接口隔离

遵循LSP的最好方法是设计小而精的接口。

5.2 单元测试

使用父类测试子类的行为:

class TestRectangle:
    def test_area_calculation(self):
        rect = self.create_rectangle(5, 4)
        assert rect.get_area() == 20
    
    def create_rectangle(self, w, h):
        return Rectangle(w, h)  # 子类可以覆盖此方法

5.3 真实案例

  • Java的Stack继承Vector:违反LSP(Stack应该只能从顶部操作)
  • Collections框架的设计:良好遵循LSP

总结

里氏替换原则告诉我们:

  1. 继承是一种承诺,而不仅仅是代码复用
  2. 契约优于类型,行为一致性是关键
  3. 测试驱动设计有助于发现LSP违规
  4. 组合优于继承往往是更安全的选择
  5. 思考可替换性,而不仅仅是"是一个"关系

在代码世界中,"正方形是矩形"确实可能是个谎言——除非你的设计能够保证它们的行为完全兼容。


参考资料

  1. Barbara Liskov (1987) - "Data Abstraction and Hierarchy"
  2. Robert C. Martin - "SOLID Principles"
  3. Martin Fowler - "Refactoring: Improving the Design of Existing Code"

相关文章

🗺️ 思维导图

思维导图

点击查看大图 🔍

加载中...