里氏替换原则(Liskov Substitution Principle, LSP)

设计模式 软考 设计模式
📅 2025-07-21 09:08 👤 admin

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的核心原则之一,由芭芭拉・利斯科夫(Barbara Liskov)在 1987 年提出。其核心思想是:子类对象必须能够替换掉它们的父类对象,而不影响程序的正确性。这意味着,子类应当遵循父类的契约,保持行为的一致性。

核心动机

  • 继承的正确性:确保子类不会破坏父类的原有功能。
  • 多态的可靠性:通过父类引用调用子类方法时,结果应符合预期。
  • 降低系统风险:避免因子类行为异常导致的潜在错误。

LSP 的关键要求

  1. 前置条件不能强化:子类方法的前置条件(输入参数)不能比父类更严格。
  2. 后置条件不能弱化:子类方法的后置条件(返回值)必须满足父类的要求。
  3. 不变式要保持:子类不能改变父类的不变量(如类的约束条件)。
  4. 异常要兼容:子类抛出的异常应当与父类一致或为其子类。

C# 示例:矩形与正方形问题

违反 LSP 的经典案例

假设我们有一个矩形类和一个正方形类,正方形继承自矩形:
// 基类:矩形
public class Rectangle
{
    public virtual double Width { get; set; }
    public virtual double Height { get; set; }

    public double Area() => Width * Height;
}

// 子类:正方形(错误设计)
public class Square : Rectangle
{
    // 正方形的宽和高必须相等
    public override double Width
    {
        get => base.Width;
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }

    public override double Height
    {
        get => base.Height;
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
}

问题分析

这个设计违反了 LSP,因为:
  • 前置条件被强化:父类允许设置不同的宽和高,但子类强制二者相等。
  • 行为不一致:以下代码在使用父类时正常,但使用子类时会出错:
    public void TestRectangle(Rectangle rect)
    {
        rect.Width = 5;
        rect.Height = 10;
        Console.WriteLine(rect.Area()); // 预期输出50
    }
    
    // 测试
    var rectangle = new Rectangle();
    TestRectangle(rectangle); // 输出50,正确
    
    var square = new Square();
    TestRectangle(square);    // 输出100,错误!

遵循 LSP 的重构方案

方案 1:抽象公共接口

// 抽象接口:四边形
public interface IQuadrilateral
{
    double Width { get; }
    double Height { get; }
    double Area();
}

// 矩形实现
public class Rectangle : IQuadrilateral
{
    public double Width { get; set; }
    public double Height { get; set; }
    public double Area() => Width * Height;
}

// 正方形实现
public class Square : IQuadrilateral
{
    private double _side;
    
    public double Width
    {
        get => _side;
        set => _side = value;
    }
    
    public double Height
    {
        get => _side;
        set => _side = value;
    }
    
    public double Area() => _side * _side;
}

方案 2:组合而非继承

// 正方形包含一个矩形实例
public class Square
{
    private readonly Rectangle _rectangle = new Rectangle();
    
    public double Side
    {
        get => _rectangle.Width;
        set
        {
            _rectangle.Width = value;
            _rectangle.Height = value;
        }
    }
    
    public double Area() => _rectangle.Area();
}

另一个示例:银行账户与透支账户

违反 LSP 的设计

// 基类:银行账户
public class BankAccount
{
    protected double _balance;
    
    public virtual void Withdraw(double amount)
    {
        if (amount > _balance)
            throw new InvalidOperationException("余额不足");
        
        _balance -= amount;
    }
}

// 子类:透支账户(错误设计)
public class OverdraftAccount : BankAccount
{
    private double _overdraftLimit = 1000;
    
    // 重写取款方法,允许透支
    public override void Withdraw(double amount)
    {
        if (amount > _balance + _overdraftLimit)
            throw new InvalidOperationException("超过透支限额");
            
        _balance -= amount;
    }
}

问题分析

  • 前置条件被弱化:子类允许更多情况下的取款(透支),而父类不允许。
  • 行为不一致:依赖父类行为的代码(如检查余额)在使用子类时会失效。

LSP 的其他应用场景

  1. 集合类中的协变与逆变:确保泛型集合的类型安全。
  2. 事件处理:子类事件的参数应与父类兼容。
  3. 数据库操作:子类的查询条件不能比父类更严格。

总结

  • LSP 是继承的契约:子类必须遵守父类的行为约定。
  • 避免破坏父类行为:重写方法时不要改变原有的语义。
  • 优先使用组合:当继承导致 LSP 违反时,考虑使用组合或接口替代。
通过遵循 LSP,代码可以保持良好的可替换性和扩展性,降低系统的耦合度。
相关笔记