目录
1.1 反例 1:正方形继承长方形 —— 数学正确,代码错误
3.3 实战场景:支付系统设计 —— 遵循 LSP 的可扩展方案

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言:为什么继承用着用着就出 BUG?

做 C++ 开发的同学大概率都遇到过这种场景:明明父类跑得好好的代码,换成子类后突然崩溃;或者新增一个子类扩展功能,却导致原有模块出现莫名其妙的异常。这不是你写的代码有语法错误,而是忽略了面向对象设计的核心准则 —— 里氏替换原则(Liskov Substitution Principle, LSP)。
里氏替换原则看似抽象,实则是解决继承乱象的 "定海神针"。它由图灵奖得主 Barbara Liskov 在 1987 年提出,本质是规范子类与父类的继承关系:任何父类能出现的地方,子类都能无缝替换,且替换后程序行为不变、逻辑正确。简单说,子类可以给父类 "加分"(扩展功能),但不能给父类 "减分"(破坏原有功能)。
很多新手把继承当成 "代码复用工具",只要两个类有共性就盲目继承,却不知这种做法会埋下巨大隐患。本文将用通俗的语言、完整的 C++ 实战案例,从 "坑在哪"" 是什么 ""怎么用" 三个维度,带你彻底掌握里氏替换原则,写出健壮、可扩展的面向对象代码。
一、触目惊心的反例:违反 LSP 的代码有多坑?

在讲理论之前,先看两个真实开发中高频出现的反例。这些场景你可能似曾相识,而问题的根源正是违反了里氏替换原则。
1.1 反例 1:正方形继承长方形 —— 数学正确,代码错误
数学中正方形是特殊的长方形,但在编程中直接继承会导致逻辑崩溃。我们用 C++ 实现这个经典反例:
#include <iostream>
using namespace std;
// 父类:长方形
class Rectangle {
protected:
int width; // 宽度
int height; // 高度
public:
// 构造函数
Rectangle(int w = 0, int h = 0) : width(w), height(h) {}
// 设置宽度(核心方法:仅修改宽度,不影响高度)
virtual void setWidth(int w) {
width = w;
}
// 设置高度(核心方法:仅修改高度,不影响宽度)
virtual void setHeight(int h) {
height = h;
}
// 计算面积
virtual int getArea() {
return width * height;
}
};
// 子类:正方形(错误继承长方形)
class Square : public Rectangle {
public:
Square(int side = 0) : Rectangle(side, side) {}
// 重写setWidth:正方形宽高必须相等,修改宽度时同时修改高度
void setWidth(int w) override {
width = w;
height = w; // 破坏父类行为:父类setWidth不修改高度
}
// 重写setHeight:同理,修改高度时同时修改宽度
void setHeight(int h) override {
width = h; // 破坏父类行为:父类setHeight不修改宽度
height = h;
}
};
// 客户端代码:计算长方形面积(依赖父类行为)
void testRectangleArea(Rectangle& rect) {
rect.setWidth(5); // 预期:宽度=5,高度不变
rect.setHeight(4); // 预期:高度=4,宽度不变
cout << "预期面积:20,实际面积:" << rect.getArea() << endl;
}
int main() {
Rectangle rect;
testRectangleArea(rect); // 输出:预期面积:20,实际面积:20(正确)
Square square;
testRectangleArea(square); // 输出:预期面积:20,实际面积:16(错误!)
return 0;
}
问题分析:
- 父类
Rectangle的核心契约是 "宽高独立修改",但子类Square重写方法时破坏了这个契约。 - 客户端代码
testRectangleArea基于父类契约开发,替换成子类后行为异常,这直接违反了里氏替换原则。 - 更隐蔽的是:如果后续有其他子类继承
Rectangle,或客户端代码修改,这个 bug 可能会扩散到整个系统。
1.2 反例 2:企鹅继承鸟类 —— 生物正确,编程错误
另一个典型场景:父类Bird有fly()方法,子类Penguin(企鹅)继承后却无法实现飞行,导致程序崩溃。
#include <iostream>
#include <stdexcept>
using namespace std;
// 父类:鸟类
class Bird {
public:
// 核心方法:飞行
virtual void fly() {
cout << "鸟类正在飞行" << endl;
}
};
// 子类:企鹅(错误继承鸟类)
class Penguin : public Bird {
public:
// 重写fly:企鹅不会飞,抛出异常
void fly() override {
throw runtime_error("企鹅不会飞!"); // 破坏父类无异常契约
}
// 企鹅特有方法:游泳
void swim() {
cout << "企鹅正在游泳" << endl;
}
};
// 客户端代码:让鸟类飞行(依赖父类fly()无异常)
void letBirdFly(Bird& bird) {
try {
bird.fly(); // 预期:正常飞行,无异常
} catch (const exception& e) {
cout << "程序出错:" << e.what() << endl;
}
}
int main() {
Bird sparrow;
letBirdFly(sparrow); // 输出:鸟类正在飞行(正确)
Penguin penguin;
letBirdFly(penguin); // 输出:程序出错:企鹅不会飞!(错误)
penguin.swim(); // 企鹅特有功能正常,但飞行功能破坏父类契约
return 0;
}
问题分析:
- 父类
Bird的fly()方法隐含 "可以正常飞行" 的契约,但子类Penguin重写后抛出异常,违反了 "替换后程序行为不变" 的要求。 - 客户端代码被迫添加异常处理,打破了原有设计的简洁性。如果后续新增
Ostrich(鸵鸟)等不会飞的鸟类,会导致更多重复的异常处理代码。
1.3 反例带来的核心启示
这两个反例暴露了违反里氏替换原则的三大危害:
- 破坏代码稳定性:子类替换父类后,原有功能异常,导致 "牵一发而动全身"。
- 增加维护成本:客户端代码需要针对子类特殊处理,违背 "开闭原则"。
- 降低代码可读性:继承关系看似合理(正方形是长方形、企鹅是鸟),但实际行为不一致,让其他开发者困惑。
而解决这些问题的关键,就是理解里氏替换原则的核心要求,并在代码设计中严格遵循。
二、里氏替换原则核心:4 个 "不能"+1 个 "必须"

里氏替换原则的官方定义是:如果 S 是 T 的子类型,那么所有使用 T 类型对象的地方,都可以用 S 类型对象替换,且不会改变程序的正确性。这个定义可以拆解为 5 个具体要求,用 C++ 开发者容易理解的语言总结就是:
2.1 核心要求 1:不能重写父类的非抽象方法
父类的非抽象方法是已经实现的具体行为,是父类契约的核心部分。子类重写这些方法,本质上是破坏父类的既定行为。
- 正确做法:子类可以扩展新方法(如企鹅的
swim()),但不能修改父类已实现的方法。 - C++ 示例:如果父类
Bird的fly()是具体实现,子类Eagle可以新增soar()(翱翔)方法,但不能重写fly()改变其核心逻辑。
2.2 核心要求 2:不能强化前置条件
前置条件是方法执行前的输入约束(如参数范围、格式要求)。子类方法的前置条件不能比父类更严格,否则会导致原本符合父类要求的输入,在子类中被拒绝。
#include <iostream>
using namespace std;
// 父类:文件读取器(前置条件:文件路径非空)
class FileReader {
public:
virtual string read(const string& path) {
if (path.empty()) {
throw invalid_argument("文件路径不能为空");
}
return "读取文件内容:" + path;
}
};
// 子类:加密文件读取器(错误:强化前置条件)
class EncryptedFileReader : public FileReader {
private:
string key; // 新增密钥参数
public:
EncryptedFileReader(const string& k) : key(k) {}
// 重写read:新增"密钥不能为空"的前置条件(比父类严格)
string read(const string& path) override {
if (key.empty()) { // 父类没有这个约束
throw invalid_argument("加密文件必须提供密钥");
}
if (path.empty()) {
throw invalid_argument("文件路径不能为空");
}
return "解密并读取文件:" + path;
}
};
// 客户端代码:使用父类读取普通文件
void processFile(FileReader& reader) {
try {
string content = reader.read("test.txt");
cout << content << endl;
} catch (const exception& e) {
cout << "处理失败:" << e.what() << endl;
}
}
int main() {
FileReader normalReader;
processFile(normalReader); // 输出:读取文件内容:test.txt(正确)
EncryptedFileReader encryptedReader(""); // 密钥为空
processFile(encryptedReader); // 输出:处理失败:加密文件必须提供密钥(错误)
return 0;
}
问题分析:
- 父类
FileReader的read()方法只要求 "路径非空",但子类新增了 "密钥非空" 的约束。 - 客户端代码按照父类契约调用(提供了合法路径),但子类因为前置条件更严格而抛出异常,违反了里氏替换原则。
2.3 核心要求 3:不能弱化后置条件
后置条件是方法执行后的输出约束(如返回值范围、数据状态)。子类方法的后置条件不能比父类更宽松,否则会导致客户端代码获取到不符合预期的结果。
#include <iostream>
#include <vector>
using namespace std;
// 父类:用户查询器(后置条件:返回非空用户列表)
class UserQuery {
public:
virtual vector<string> getActiveUsers() {
return {"张三", "李四", "王五"}; // 保证返回3个活跃用户
}
};
// 子类:简化用户查询器(错误:弱化后置条件)
class SimpleUserQuery : public UserQuery {
public:
// 重写getActiveUsers:返回空列表(违反父类非空契约)
vector<string> getActiveUsers() override {
return {}; // 后置条件比父类宽松
}
};
// 客户端代码:依赖父类返回非空列表
void printActiveUsers(UserQuery& query) {
vector<string> users = query.getActiveUsers();
// 父类契约保证非空,未做空判断
for (const auto& user : users) {
cout << "活跃用户:" << user << endl;
}
}
int main() {
UserQuery normalQuery;
printActiveUsers(normalQuery); // 输出3个用户(正确)
SimpleUserQuery simpleQuery;
printActiveUsers(simpleQuery); // 无输出(逻辑异常,若后续有users[0]会崩溃)
return 0;
}
问题分析:
- 父类
UserQuery的getActiveUsers()隐含 "返回非空列表" 的后置条件,客户端代码基于此设计,未做空判断。 - 子类返回空列表,弱化了后置条件,虽然语法正确,但会导致客户端代码逻辑异常(若后续访问列表元素会直接崩溃)。
2.4 核心要求 4:不能抛出额外的异常
父类方法声明的异常类型,定义了客户端需要处理的异常范围。子类方法抛出的异常不能超出父类的异常体系,否则客户端会遇到未预期的异常。
C++ 中这一要求表现为:子类重写方法抛出的异常,必须是父类方法异常的子类(或相同类型),不能抛出更宽泛的异常。
#include <iostream>
#include <stdexcept>
using namespace std;
// 父类:数据处理器(仅抛出runtime_error)
class DataProcessor {
public:
virtual void process() {
throw runtime_error("数据处理失败");
}
};
// 子类:网络数据处理器(错误:抛出额外异常)
class NetworkDataProcessor : public DataProcessor {
public:
// 重写process:抛出logic_error(父类未声明)
void process() override {
throw logic_error("网络连接超时"); // 额外异常
}
};
// 客户端代码:仅处理父类声明的runtime_error
void handleData(DataProcessor& processor) {
try {
processor.process();
} catch (const runtime_error& e) {
cout << "处理已知异常:" << e.what() << endl;
}
}
int main() {
DataProcessor localProcessor;
handleData(localProcessor); // 输出:处理已知异常:数据处理失败(正确)
NetworkDataProcessor networkProcessor;
handleData(networkProcessor); // 程序崩溃:未捕获logic_error(错误)
return 0;
}
2.5 核心要求 5:必须保持 "is-a" 关系
继承的本质是 "is-a"(是一个)的关系,但这里的 "是" 不是现实世界的分类,而是编程中的行为契约。
- 正确的 "is-a":麻雀是鸟(麻雀能飞,符合鸟的飞行契约)。
- 错误的 "is-a":企鹅是鸟(企鹅不能飞,不符合鸟的飞行契约)。
- C++ 设计启示:当子类无法完全遵守父类的行为契约时,说明继承关系设计错误,应放弃继承,改用其他方案(如组合、接口分离)。
三、C++ 实战:如何正确实现里氏替换原则?

理解了核心要求后,关键是在实际开发中落地。下面通过 "修复反例" 和 "实战场景" 两个维度,给出完整的 C++ 实现方案。
3.1 修复反例 1:正方形与长方形 —— 用组合替代继承
正方形继承长方形的问题,根源是两者的行为契约不一致。正确的做法是:提取共同基类,用组合而非继承实现共性复用。
#include <iostream>
using namespace std;
// 抽象基类:图形(提取长方形和正方形的共性)
class Shape {
public:
virtual int getArea() const = 0; // 纯虚方法:计算面积(统一契约)
virtual ~Shape() {} // 虚析构函数:确保子类正确析构
};
// 子类:长方形(遵循Shape契约)
class Rectangle : public Shape {
private:
int width;
int height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int getArea() const override { return width * height; }
};
// 子类:正方形(遵循Shape契约)
class Square : public Shape {
private:
int side;
public:
Square(int s) : side(s) {}
void setSide(int s) { side = s; }
int getArea() const override { return side * side; }
};
// 客户端代码:依赖Shape抽象基类(符合LSP)
void testShapeArea(const Shape& shape) {
cout << "图形面积:" << shape.getArea() << endl;
}
int main() {
Rectangle rect(5, 4);
testShapeArea(rect); // 输出:图形面积:20(正确)
Square square(4);
testShapeArea(square); // 输出:图形面积:16(正确)
// 替换后程序行为一致,符合里氏替换原则
rect.setWidth(6);
testShapeArea(rect); // 输出:图形面积:24(正确)
square.setSide(5);
testShapeArea(square); // 输出:图形面积:25(正确)
return 0;
}
修复思路:
- 提取抽象基类
Shape,定义统一的getArea()契约,避免父类包含子类无法遵守的方法(如setWidth)。 - 长方形和正方形各自继承
Shape,实现符合自身特性的方法,不再互相继承。 - 客户端代码依赖抽象基类,替换任何子类都不会改变行为,完美符合 LSP。
3.2 修复反例 2:企鹅与鸟类 —— 用接口分离行为
企鹅不会飞的问题,根源是父类Bird包含了子类无法实现的行为。正确的做法是:将特殊行为(飞行)分离为接口,子类按需实现。
#include <iostream>
using namespace std;
// 抽象基类:鸟类(包含所有鸟类的共性行为)
class Bird {
public:
virtual void eat() const {
cout << "鸟类正在进食" << endl;
}
virtual ~Bird() {}
};
// 接口:飞行能力(仅会飞的鸟类实现)
class Flyable {
public:
virtual void fly() const = 0;
virtual ~Flyable() {}
};
// 子类:麻雀(是鸟,且会飞)
class Sparrow : public Bird, public Flyable {
public:
void eat() const override {
cout << "麻雀吃虫子" << endl;
}
void fly() const override {
cout << "麻雀正在低空飞行" << endl;
}
};
// 子类:企鹅(是鸟,但不会飞)
class Penguin : public Bird {
public:
void eat() const override {
cout << "企鹅吃鱼" << endl;
}
void swim() const {
cout << "企鹅正在南极游泳" << endl;
}
};
// 子类:鹰(是鸟,且会飞)
class Eagle : public Bird, public Flyable {
public:
void eat() const override {
cout << "鹰吃肉类" << endl;
}
void fly() const override {
cout << "鹰正在高空翱翔" << endl;
}
};
// 客户端代码1:处理所有鸟类(调用共性行为eat)
void feedBird(const Bird& bird) {
bird.eat();
}
// 客户端代码2:处理会飞的鸟(调用飞行行为)
void letFly(const Flyable& flyable) {
flyable.fly();
}
int main() {
Sparrow sparrow;
Penguin penguin;
Eagle eagle;
// 所有鸟类都能被feedBird处理(符合LSP)
feedBird(sparrow); // 输出:麻雀吃虫子
feedBird(penguin); // 输出:企鹅吃鱼
feedBird(eagle); // 输出:鹰吃肉类
// 只有会飞的鸟能被letFly处理(无异常)
letFly(sparrow); // 输出:麻雀正在低空飞行
letFly(eagle); // 输出:鹰正在高空翱翔
// penguin没有实现Flyable,无法传入letFly(编译时检查,避免运行时错误)
// letFly(penguin); // 编译报错:无法转换参数类型
penguin.swim(); // 企鹅特有功能正常使用
return 0;
}
修复思路:
- 拆分行为:将
Bird类的fly()方法分离为Flyable接口,实现 "行为与基类分离"。 - 按需实现:会飞的鸟类(麻雀、鹰)同时继承
Bird和Flyable,不会飞的鸟类(企鹅)仅继承Bird。 - 编译时检查:客户端代码调用飞行功能时,依赖
Flyable接口而非Bird类,避免将不会飞的鸟传入,从根源上杜绝运行时异常。
3.3 实战场景:支付系统设计 —— 遵循 LSP 的可扩展方案
下面设计一个电商支付系统,展示里氏替换原则在实际项目中的应用。需求是:支持多种支付方式(信用卡、微信、支付宝),且新增支付方式时不修改原有代码。
设计思路:
- 抽象基类
Payment:定义统一的支付契约(pay()方法)。 - 子类实现:每种支付方式作为子类,实现自身的支付逻辑,但不改变父类契约。
- 客户端代码:依赖
Payment基类,替换任何子类都能正常工作。
#include <iostream>
#include <string>
using namespace std;
// 支付结果结构体:统一返回格式(后置条件一致)
struct PaymentResult {
bool success; // 支付是否成功
string orderId; // 订单号
string message; // 提示信息
};
// 抽象基类:支付方式(定义统一契约)
class Payment {
protected:
string orderId;
double amount;
public:
Payment(const string& oid, double amt) : orderId(oid), amount(amt) {}
// 纯虚方法:支付(前置条件:订单号非空、金额>0;后置条件:返回PaymentResult)
virtual PaymentResult pay() const = 0;
virtual ~Payment() {}
};
// 子类1:信用卡支付
class CreditCardPayment : public Payment {
private:
string cardNumber; // 卡号(子类特有属性)
public:
CreditCardPayment(const string& oid, double amt, const string& cardNo)
: Payment(oid, amt), cardNumber(cardNo) {}
PaymentResult pay() const override {
// 模拟信用卡支付逻辑(不改变父类契约)
if (cardNumber.empty() || amount <= 0) {
return {false, orderId, "支付失败:卡号为空或金额无效"};
}
// 模拟支付成功
return {true, orderId, "信用卡支付成功:" + cardNumber.substr(cardNumber.size()-4)};
}
};
// 子类2:微信支付
class WeChatPayment : public Payment {
private:
string openId; // 微信openId(子类特有属性)
public:
WeChatPayment(const string& oid, double amt, const string& oid)
: Payment(oid, amt), openId(oid) {}
PaymentResult pay() const override {
// 模拟微信支付逻辑(不改变父类契约)
if (openId.empty() || amount <= 0) {
return {false, orderId, "支付失败:openId为空或金额无效"};
}
// 模拟支付成功
return {true, orderId, "微信支付成功:" + openId.substr(0, 8) + "***"};
}
};
// 子类3:支付宝支付(新增支付方式,不修改原有代码)
class AlipayPayment : public Payment {
private:
string alipayAccount; // 支付宝账号(子类特有属性)
public:
AlipayPayment(const string& oid, double amt, const string& account)
: Payment(oid, amt), alipayAccount(account) {}
PaymentResult pay() const override {
// 模拟支付宝支付逻辑(遵循父类契约)
if (alipayAccount.empty() || amount <= 0) {
return {false, orderId, "支付失败:支付宝账号为空或金额无效"};
}
// 模拟支付成功
return {true, orderId, "支付宝支付成功:" + alipayAccount};
}
};
// 客户端代码:订单支付(依赖Payment基类,符合LSP)
void processOrderPayment(const Payment& payment) {
cout << "正在处理订单:" << payment.getOrderId() << endl;
PaymentResult result = payment.pay();
cout << "支付结果:" << (result.success ? "成功" : "失败") << endl;
cout << "提示信息:" << result.message << endl;
cout << "------------------------" << endl;
}
// 扩展:获取订单号(父类新增方法,子类自动继承,不破坏LSP)
string Payment::getOrderId() const {
return orderId;
}
int main() {
// 信用卡支付
CreditCardPayment creditPay("ORDER_20250101_001", 99.9, "622202********1234");
processOrderPayment(creditPay);
// 微信支付
WeChatPayment wechatPay("ORDER_20250101_002", 199.0, "o6_bmjrPTlm6_2sgVt7hMZOPfL2M");
processOrderPayment(wechatPay);
// 支付宝支付(新增子类,客户端代码无需修改)
AlipayPayment alipayPay("ORDER_20250101_003", 299.5, "zhangsan@163.com");
processOrderPayment(alipayPay);
return 0;
}
代码亮点:
- 契约一致性:所有子类都遵循
Payment基类的契约,前置条件(订单号非空、金额 > 0)和后置条件(返回PaymentResult)保持一致。 - 可扩展性:新增
AlipayPayment子类时,无需修改客户端代码processOrderPayment,符合 "开闭原则"。 - 兼容性:父类新增
getOrderId()方法时,子类自动继承,不会破坏现有代码,体现了 LSP 的灵活性。 - 可读性:继承关系清晰,每个子类的职责明确,其他开发者可以快速理解和扩展。
四、LSP 与其他设计原则的关联:构建完整的设计体系

里氏替换原则不是孤立的,它与 SOLID 其他原则(尤其是开闭原则、依赖倒置原则)紧密关联,共同构成面向对象设计的核心体系。
4.1 LSP 是开闭原则的基础
开闭原则要求 "对扩展开放,对修改关闭"。而里氏替换原则通过保证子类替换的兼容性,让扩展(新增子类)不会影响原有代码,从而实现开闭原则。
- 反例:如果子类违反 LSP,新增子类后需要修改客户端代码(如添加异常处理、特殊判断),这直接违背了开闭原则。
- 正例:支付系统中新增支付宝支付子类,无需修改客户端的订单处理代码,同时满足 LSP 和开闭原则。
4.2 LSP 与依赖倒置原则相辅相成
依赖倒置原则要求 "高层模块依赖抽象,不依赖具体实现"。而 LSP 保证了抽象基类的子类都能符合抽象契约,让高层模块可以放心依赖抽象。
- 关系:依赖倒置原则解决 "依赖谁" 的问题,里氏替换原则解决 "依赖后是否可靠" 的问题。
- 示例:支付系统的客户端代码
processOrderPayment依赖抽象基类Payment(依赖倒置),而 LSP 保证任何Payment子类都能正常工作,两者结合让代码既灵活又健壮。
4.3 LSP 与接口隔离原则的互补
接口隔离原则要求 "客户端不依赖不需要的接口"。里氏替换原则则要求 "子类必须实现接口的所有契约",两者互补:
- 接口隔离原则:拆分臃肿接口,避免子类被迫实现不需要的方法(如将
fly()从Bird类拆分出来)。 - 里氏替换原则:确保子类实现的接口方法符合契约(如
Sparrow的fly()方法正常工作)。
五、C++ 开发中遵循 LSP 的实用技巧

除了上述原则和示例,在实际 C++ 开发中,还可以通过以下技巧高效遵循里氏替换原则:
5.1 优先使用组合而非继承
当不确定子类是否能完全遵守父类契约时,组合是比继承更安全的选择。组合的核心是 "has-a"(有一个)关系,而非 "is-a" 关系。
// 不推荐:继承可能违反LSP
class BadEncryptedFileReader : public FileReader {
// 可能强化前置条件、修改父类行为
};
// 推荐:组合遵循LSP
class GoodEncryptedFileReader {
private:
FileReader fileReader; // 组合FileReader,复用其功能
string key;
public:
string read(const string& path, const string& k) {
key = k;
if (key.empty()) throw invalid_argument("密钥不能为空");
return decrypt(fileReader.read(path)); // 不修改FileReader行为
}
};
优势:
- 组合不会破坏原有类的契约,避免违反 LSP。
- 灵活性更高:可以选择调用原有类的方法,或添加额外逻辑,无需重写。
5.2 用抽象基类和纯虚方法定义契约
C++ 中通过抽象基类(包含纯虚方法)可以明确契约,强制子类实现必要的方法,同时避免父类包含子类无法遵守的具体行为。
- 技巧:将父类设计为抽象基类,仅包含纯虚方法和共性属性,具体实现放在子类中。
- 示例:支付系统的
Payment类、图形系统的Shape类,都是通过纯虚方法定义契约。
5.3 编写单元测试验证 LSP 符合性
一个简单有效的验证方法:为父类编写单元测试,然后用子类对象替换父类对象,所有测试用例必须通过。
#include <gtest/gtest.h>
#include "Shape.h"
// 父类测试用例
TEST(ShapeTest, RectangleArea) {
Rectangle rect(5, 4);
EXPECT_EQ(rect.getArea(), 20);
rect.setWidth(6);
EXPECT_EQ(rect.getArea(), 24);
}
// 子类替换测试(验证LSP)
TEST(ShapeTest, SquareArea) {
Square square(4);
EXPECT_EQ(square.getArea(), 16); // 父类测试用例适配子类
square.setSide(5);
EXPECT_EQ(square.getArea(), 25);
}
优势:
- 自动化验证子类是否符合 LSP,提前发现潜在问题。
- 确保后续修改(如子类重构)不会破坏契约。
5.4 避免在子类中新增带约束的方法
子类可以新增方法,但新增方法不能带有比父类更严格的约束,否则会让客户端代码在使用子类特有方法时产生困惑。
- 错误示例:子类新增方法要求参数必须为正数,而父类类似方法无此约束。
- 正确示例:子类新增方法的约束与父类一致,或更宽松。
六、总结:里氏替换原则的本质是 "契约精神"

里氏替换原则看似是关于继承的规则,实则是面向对象设计的 "契约精神"—— 父类定义契约,子类遵守契约,客户端依赖契约。
核心要点回顾:
- 一句话记住 LSP:父类能处,子类就能处,且表现一样好。
- 四大禁忌:不重写父类非抽象方法、不强化前置条件、不弱化后置条件、不抛出额外异常。
- 两大方案:违反 LSP 时,优先用 "组合替代继承" 或 "接口分离行为"。
- 验证方法:子类替换父类后,原有代码无异常、单元测试全通过。
遵循里氏替换原则,可能会让你在设计初期多花一些时间思考继承关系和契约,但从长远来看,它能大幅降低代码的维护成本、减少 BUG、提高扩展性。当你下次想写class A : public B时,不妨先问自己:子类能完全遵守父类的契约吗?替换后程序行为会变吗?
设计模式的核心不是死记硬背规则,而是培养一种 "优雅解决问题" 的思维。里氏替换原则作为 SOLID 的重要组成部分,是你从 "能写出运行的代码" 到 "能写出好维护的代码" 的关键一步。
欢迎在评论区分享你在项目中遇到的违反 LSP 的坑,以及你是如何解决的!
3280

被折叠的 条评论
为什么被折叠?



