在代码中,为什么说"正方形是矩形"是个谎言?里氏替换原则(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_width和set_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
总结
里氏替换原则告诉我们:
- 继承是一种承诺,而不仅仅是代码复用
- 契约优于类型,行为一致性是关键
- 测试驱动设计有助于发现LSP违规
- 组合优于继承往往是更安全的选择
- 思考可替换性,而不仅仅是"是一个"关系
在代码世界中,"正方形是矩形"确实可能是个谎言——除非你的设计能够保证它们的行为完全兼容。
参考资料
- Barbara Liskov (1987) - "Data Abstraction and Hierarchy"
- Robert C. Martin - "SOLID Principles"
- Martin Fowler - "Refactoring: Improving the Design of Existing Code"
相关文章
🗺️ 思维导图

点击查看大图 🔍
加载中...
