【设计模式】设计原则之里氏替换原则

里氏替换原则是面向对象设计的核心准则之一,由Barbara Liskov在1987年提出,主要解决继承关系的正确性问题。其核心思想是:

子类型必须能够替换其基类型而不引起程序错误或行为异常。​
(If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program.)

核心理解

  1. 行为一致性​:子类必须完全实现父类的契约(方法行为和约束)
  2. 继承语义​:"is-a"关系应表示行为可替换性,而不仅仅是数据结构的包含
  3. 设计警示​:当子类无法自然替换父类时,说明继承关系设计有缺陷

关键规则与C++实现

1. 方法签名规则
class Bird {
public:
    virtual void fly() const {
        std::cout << "Flying..." << std::endl;
    }
};

// ✅ 遵循LSP:强化后置条件(飞行高度>0)
class Eagle : public Bird {
public:
    void fly() const override {
        std::cout << "Flying at 1000m altitude" << std::endl;
    }
};

// 🚫 违反LSP:弱化后置条件(不会飞)
class Penguin : public Bird {
public:
    void fly() const override {
        throw std::runtime_error("Penguins can't fly!");
    }
};

// 客户端代码
void travel(Bird& bird) {
    bird.fly(); // 若传入Penguin程序崩溃
}

解决方案: 重构继承关系

class Bird {}; // 基类无飞行方法

class FlyingBird : public Bird {
public:
    virtual void fly() const = 0;
};

class Eagle : public FlyingBird { /* 实现fly */ };
class Penguin : public Bird {}; // 无需实现fly
2. 不变量保护规则
class Rectangle {
protected:
    int width, height;
public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }
    int getArea() const { return width * height; }
};

// 🚫 违反LSP:破坏矩形宽高独立变化的不变量
class Square : public Rectangle {
public:
    void setWidth(int w) override {
        width = height = w; // 同时修改高度
    }
    void setHeight(int h) override {
        width = height = h; // 同时修改宽度
    }
};

void testArea(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(4);
    assert(r.getArea() == 20); // 对Square会失败
}

解决方案: 放弃继承关系

class Shape {
public:
    virtual int getArea() const = 0;
};

class Rectangle : public Shape { /* 独立实现 */ };
class Square : public Shape { /* 独立实现 */ };
3. 前置条件规则
class FileReader {
public:
    // 前置:文件必须可读
    virtual std::string read(const std::string& path) {
        if (!isReadable(path)) throw std::exception();
        return readFile(path);
    }
};

// 🚫 违反LSP:强化前置条件(加密文件需密钥)
class EncryptedFileReader : public FileReader {
public:
    // 添加新前置条件(必须提供密钥)
    std::string read(const std::string& path) override {
        if (key_.empty()) throw std::exception();
        return decrypt(super::read(path));
    }
    
private:
    std::string key_;
};

void processFile(FileReader& reader) {
    auto data = reader.read("normal.txt"); 
    // 传入EncryptedFileReader会意外报错
}

解决方案: 使用组合而非继承

class EncryptedFileProcessor {
public:
    EncryptedFileProcessor(FileReader& reader) : reader_(reader) {}
    
    std::string readEncrypted(const std::string& path, const std::string& key) {
        // ...
    }
private:
    FileReader& reader_;
};

LSP在C++中的关键应用

1. 多态集合处理
void drawShapes(const std::vector<std::shared_ptr<Shape>>& shapes) {
    for (const auto& shape : shapes) {
        shape->draw(); // 所有子类必须可靠实现
    }
}

// 若Ellipse子类修改了draw行为(如触发附加动画)
// 必须保持基础绘图功能不变
2. 智能指针的正确使用
std::unique_ptr<Database> createDatabase(bool useSQLite) {
    if (useSQLite) {
        return std::make_unique<SQLiteDatabase>();
    } else {
        return std::make_unique<PostgreSQLDatabase>();
    }
}

auto db = createDatabase(true);
db->query("SELECT *"); // 不同子类行为必须一致

检测LSP违反的实践方法

  1. 单元测试套件继承
// 基类测试
class BirdTest : public testing::Test {
protected:
    std::unique_ptr<Bird> bird;
};

TEST_F(BirdTest, FlightTest) {
    bird->fly(); // 应无异常
}

// 子类测试继承所有基类测试
class EagleTest : public BirdTest {
protected:
    EagleTest() {
        bird = std::make_unique<Eagle>();
    }
};

// Penguin无法通过基类测试,提示继承错误
  1. 设计时契约检查
class Shape {
public:
    virtual void scale(double factor) {
        // 契约:缩放后面积变为 factor^2 倍
        const double originalArea = getArea();
        // ...缩放实现...
        assert(std::abs(getArea()/originalArea - factor*factor) < 1e-9);
    }
};

LSP与其他原则的关系

原则关联点C++实现技巧
OCP违反LSP会破坏OCP的扩展性通过抽象接口规范子类扩展
ISP肥大的接口更可能导致LSP违反拆分精细接口减少错误覆盖
DIP依赖抽象可自然保持LSP合规高层模块通过基类引用使用对象

总结:LSP价值矩阵

方面违反LSP的代价遵循LSP的收益
可维护性意外行为增加调试难度多态调用可靠,逻辑一致
可扩展性添加新子类导致连锁问题新子类安全替换,满足OCP
代码复用父类方法在子类中部分不可用继承真正实现“is-a”关系
测试成本需要为每个子类重写测试共享基类测试,减少用例数量

实施要点:

  • 慎用继承,优先组合
  • 子类扩展行为,不要修改原有契约
  • 通过设计契约(前置/后置条件、不变量)验证替换性
  • 使用C++11的override关键字明确表示重写

里氏替换原则本质是对继承关系的质量保证,确保多态机制不会成为系统的不稳定因素。在C++中,结合虚函数规范、智能指针管理和合约式设计,可构建出真正符合LSP的健壮继承体系。

参考:

  1. 【设计模式】设计原则之开闭原则-CSDN博客
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浩瀚之水_csdn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值