5架构整洁之道

里氏替换原则

LSP

子类型契约行为一致性is-a关系

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第5章 里氏替换原则 - LSP (Liskov Substitution Principle)

一、导读

1.1 本章学习目标

通过本章的学习,你将能够:

深入理解里氏替换原则(LSP)的数学定义和实际含义

识别代码中违反 LSP 的模式,特别是继承层次中的行为不一致

掌握设计符合 LSP 的继承关系的方法

理解行为子类型化与名义子类型化的区别

学会使用契约式设计(前置条件、后置条件、不变式)验证 LSP

理解"组合优于继承"的设计哲学

掌握在无法避免 LSP 违反时的替代方案

理解 LSP 与多态、开闭原则的关系

1.2 为什么本章如此重要

里氏替换原则是 SOLID 原则中最微妙也最容易被忽视的一个。它由 Barbara Liskov 在 1987 年提出,最初是一个关于类型理论的数学概念,后来被 Robert C. Martin 引入到面向对象设计中。

LSP 的核心洞察是:继承不仅仅是代码复用机制,它还建立了一种行为契约。当类 B 继承类 A 时,B 承诺在所有情况下都能替代 A 而不改变程序的正确性。如果这个承诺被打破,多态就会成为陷阱而非优势。

在实际开发中,违反 LSP 是导致多态滥用、继承层次脆弱、运行时错误的主要原因之一。经典的"正方形-矩形问题"就是一个典型例子:从几何学角度看,正方形确实是特殊的矩形;但从软件设计角度看,让 Square 继承 Rectangle 会导致行为不一致。

理解 LSP 有助于我们做出更好的继承决策,避免将继承仅仅作为代码共享的手段。它也促使我们思考接口与实现的关系,以及如何使用组合来替代继承。

1.3 核心问题引导

在深入本章内容之前,请思考以下问题:

什么是"子类型必须能够替换基类型"?如何判断是否可以安全替换?

为什么"正方形继承矩形"是 LSP 的经典违反案例?

继承除了代码复用,还建立了什么样的关系?

如何识别继承层次中的 LSP 违反?有哪些信号?

当前置条件、后置条件或不变式被改变时,会发生什么?

什么时候应该使用组合而非继承?

接口继承和实现继承有什么区别?

在函数式编程中,LSP 有对应的概念吗?


二、核心概念详解

2.1 LSP 的正式定义

里氏替换原则的正式表述是:如果对每个类型为 S 的对象 o1,都存在一个类型为 T 的对象 o2,使得对于所有用 T 编写的程序 P,将 o1 替换为 o2 后程序的行为不变,那么 S 就是 T 的子类型。

这个定义看似复杂,但核心思想很简单:子类应该能够在任何父类出现的地方正常工作,而不会让调用者察觉到差异。

更实用的表述是:子类不应该改变父类的契约。这里的"契约"包括:

  • 前置条件:方法执行前必须满足的条件
  • 后置条件:方法执行后保证成立的条件
  • 不变式:对象在整个生命周期中保持不变的属性

2.2 契约式设计

Bertrand Meyer 提出的契约式设计(Design by Contract)为理解 LSP 提供了清晰的框架。

前置条件(Precondition):方法执行前必须为真的条件。子类不能加强前置条件(即要求更严格的输入),因为这会使得原本对父类有效的输入对子类无效。

java
// 父类
public class Stack {
    /**
     * 前置条件:无(可以 push 任何元素)
     */
    public void push(Object item) {
        // ...
    }
}

// 违反 LSP 的子类
public class BoundedStack extends Stack {
    private int maxSize;
    
    /**
     * 前置条件:栈未满
     * 这加强了前置条件,违反 LSP
     */
    @Override
    public void push(Object item) {
        if (size() >= maxSize) {
            throw new IllegalStateException("Stack is full");
        }
        super.push(item);
    }
}

后置条件(Postcondition):方法执行后保证为真的条件。子类不能削弱后置条件(即保证更少),因为这会使得调用者无法依赖父类所保证的结果。

java
// 父类
public class SearchService {
    /**
     * 后置条件:返回所有匹配的结果
     */
    public List<Result> search(String query) {
        // 返回完整结果
    }
}

// 违反 LSP 的子类
public class CachedSearchService extends SearchService {
    /**
     * 后置条件:可能只返回缓存中的部分结果
     * 这削弱了后置条件,违反 LSP
     */
    @Override
    public List<Result> search(String query) {
        if (cache.containsKey(query)) {
            return cache.get(query); // 可能不是完整结果
        }
        return super.search(query);
    }
}

不变式(Invariant):对象在整个生命周期中保持为真的属性。子类不能破坏父类的不变式。

java
// 父类
public class PositiveCounter {
    private int count = 0;
    
    /**
     * 不变式:count >= 0
     */
    public void increment() {
        count++;
    }
    
    public void decrement() {
        if (count > 0) {
            count--;
        }
    }
    
    public int getCount() {
        return count;
    }
}

// 违反 LSP 的子类
public class UnboundedCounter extends PositiveCounter {
    /**
     * 允许 count 变为负数,破坏了父类的不变式
     */
    @Override
    public void decrement() {
        // 移除了 count > 0 的检查
        super.decrement(); // 但父类方法仍有检查,所以这里需要直接操作
    }
}

2.3 经典的 LSP 违反案例

案例一:正方形-矩形问题

这是最著名的 LSP 违反案例。从几何学角度,正方形是特殊的矩形(四边相等的矩形)。但从软件设计角度,让 Square 继承 Rectangle 会导致问题:

java
// 违反 LSP 的设计
public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    
    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 正方形宽高必须相等
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height; // 正方形宽高必须相等
        this.height = height;
    }
}

// 测试代码
public void testRectangle(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.getArea() == 50 : "Expected area 50, got " + r.getArea();
}

// 传入 Rectangle 时测试通过
// 传入 Square 时测试失败(area = 100)

问题的根源在于:Rectangle 的契约允许独立设置宽度和高度,但 Square 打破了这个契约。

案例二:只读集合

java
// 违反 LSP
public class ReadOnlyList<T> extends ArrayList<T> {
    @Override
    public void add(T element) {
        throw new UnsupportedOperationException("Read-only list");
    }
    
    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException("Read-only list");
    }
}

// 这段代码在使用 ArrayList 时正常工作,但传入 ReadOnlyList 时会崩溃
public void populateList(List<String> list) {
    list.add("item1");
    list.add("item2");
}

案例三:抛出额外异常

java
// 违反 LSP
public class BaseService {
    public void process() throws IOException {
        // 可能抛出 IOException
    }
}

public class ExtendedService extends BaseService {
    @Override
    public void process() throws IOException, SQLException {
        // 抛出了父类未声明的额外异常
        // 调用者如果只捕获 IOException,可能会漏掉 SQLException
    }
}

2.4 识别 LSP 违反的信号

以下是代码中可能违反 LSP 的信号:

1. instanceof 检查

java
if (shape instanceof Square) {
    // 特殊处理 Square
} else if (shape instanceof Circle) {
    // 特殊处理 Circle
}

如果客户端代码需要检查具体类型并特殊处理,说明子类不能完全替代父类。

2. 空实现或抛出异常的方法

java
@Override
public void someMethod() {
    throw new UnsupportedOperationException();
}

这通常意味着子类不需要或不支持这个方法,违反了父类的契约。

3. 文档中的警告

java
/**
 * 注意:此实现在某些条件下可能不返回结果
 */
@Override
public List<Result> search(String query) {
    // ...
}

如果子类需要在文档中警告调用者注意不同的行为,说明契约被改变了。

4. 重写方法改变语义

即使方法签名相同,如果行为语义不同,也违反 LSP。

2.5 符合 LSP 的设计策略

策略一:重新建模继承关系

如果子类不能真正替代父类,考虑重新设计继承层次。

java
// 不符合 LSP
class Bird {
    void fly() { /* ... */ }
}
class Penguin extends Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

// 符合 LSP:重新建模
interface Animal {
    void move();
}

interface Flyable {
    void fly();
}

class Bird implements Animal, Flyable {
    public void move() { fly(); }
    public void fly() { /* ... */ }
}

class Penguin implements Animal {
    public void move() { swim(); }
    public void swim() { /* ... */ }
}

策略二:使用组合而非继承

java
// 不符合 LSP:继承用于代码复用
class Stack extends ArrayList {
    // 继承了 ArrayList 的所有方法,但很多不适用
}

// 符合 LSP:使用组合
class Stack {
    private final List<Object> elements = new ArrayList<>();
    
    public void push(Object item) {
        elements.add(item);
    }
    
    public Object pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
}

策略三:使用接口而非抽象类

接口只定义契约,不提供实现,避免了实现继承带来的 LSP 问题。

策略四:使用委托

java
public class LoggingList<T> implements List<T> {
    private final List<T> delegate;
    private final Logger logger;
    
    public LoggingList(List<T> delegate, Logger logger) {
        this.delegate = delegate;
        this.logger = logger;
    }
    
    @Override
    public void add(T element) {
        logger.info("Adding: " + element);
        delegate.add(element);
    }
    
    // 其他方法委托给 delegate
}

2.6 LSP 与多态的关系

LSP 是多态正确性的保证。只有当子类符合 LSP 时,多态才能安全工作。

java
// 如果所有 Shape 子类都符合 LSP,这段代码可以安全处理任何 Shape
public double calculateTotalArea(List<Shape> shapes) {
    return shapes.stream()
        .mapToDouble(Shape::getArea)
        .sum();
}

如果某个子类违反 LSP(例如返回错误的面积,或抛出异常),整个多态机制就会失效。

2.7 LSP 与其他原则的关系

LSP 与 OCP:如果子类不能正确替换父类,通过多态扩展就会出问题。LSP 是 OCP 的前提。

LSP 与 DIP:依赖倒置要求高层模块依赖抽象。如果具体实现不能正确替代抽象(违反 LSP),DIP 就失去了意义。

LSP 与 SRP:当一个类承担多个职责时,子类可能只需要其中一部分,导致其他方法成为空实现或抛出异常,违反 LSP。


三、程序员的视角

3.1 在实际项目中的应用

在日常工作中,应用 LSP 可以从以下几个方面入手:

设计继承关系时:

  • 问自己:"子类是否真的是父类的一种?"
  • 检查子类是否需要禁用或特殊处理父类的某些方法
  • 确保子类遵守父类的契约

代码审查时:

  • 寻找 instanceof 检查
  • 注意抛出 UnsupportedOperationException 的重写方法
  • 检查文档中是否有行为变化的警告

重构遗留代码时:

  • 将深层继承层次展平
  • 用组合替换继承
  • 提取接口代替抽象类

3.2 常见陷阱与误区

陷阱一:将继承视为代码复用机制

继承应该表示"is-a"关系,而不仅仅是共享代码。如果只是需要代码复用,优先考虑组合。

陷阱二:忽视行为语义

即使方法签名相同,如果行为语义不同,也违反 LSP。仔细阅读父类的文档,理解其契约。

陷阱三:过度使用抽象类

抽象类混合了接口定义和实现共享,容易导致 LSP 问题。优先使用接口定义契约,使用组合共享实现。

3.3 与其他层级的联系

LSP 与软件工程的其他方面密切相关:

  • 测试:符合 LSP 的代码可以用父类的测试用例测试子类
  • 文档:清晰的契约文档有助于确保 LSP
  • IDE 支持:现代 IDE 可以检测潜在的 LSP 违反

四、代码实战

4.1 示例一:修复正方形-矩形问题

java
/**
 * 符合 LSP 的形状设计
 */

// === 抽象:形状接口 ===

public interface Shape {
    /**
     * 计算面积
     * @return 形状的面积
     */
    double getArea();
    
    /**
     * 计算周长
     * @return 形状的周长
     */
    double getPerimeter();
}

// === 具体实现 ===

public class Rectangle implements Shape {
    private final double width;
    private final double height;
    
    public Rectangle(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Dimensions must be positive");
        }
        this.width = width;
        this.height = height;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    @Override
    public double getArea() {
        return width * height;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

public class Square implements Shape {
    private final double side;
    
    public Square(double side) {
        if (side <= 0) {
            throw new IllegalArgumentException("Side must be positive");
        }
        this.side = side;
    }
    
    public double getSide() { return side; }
    
    @Override
    public double getArea() {
        return side * side;
    }
    
    @Override
    public double getPerimeter() {
        return 4 * side;
    }
}

public class Circle implements Shape {
    private final double radius;
    
    public Circle(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius must be positive");
        }
        this.radius = radius;
    }
    
    public double getRadius() { return radius; }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

// === 使用多态的代码 ===

public class ShapeCalculator {
    
    public double calculateTotalArea(List<Shape> shapes) {
        return shapes.stream()
            .mapToDouble(Shape::getArea)
            .sum();
    }
    
    public double calculateTotalPerimeter(List<Shape> shapes) {
        return shapes.stream()
            .mapToDouble(Shape::getPerimeter)
            .sum();
    }
    
    public Shape findLargest(List<Shape> shapes) {
        return shapes.stream()
            .max(Comparator.comparingDouble(Shape::getArea))
            .orElseThrow(() -> new IllegalArgumentException("No shapes provided"));
    }
}

// === 测试 ===

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class ShapeTest {
    
    @Test
    void rectangleArea() {
        Rectangle rect = new Rectangle(5, 10);
        assertEquals(50, rect.getArea());
    }
    
    @Test
    void squareArea() {
        Square square = new Square(5);
        assertEquals(25, square.getArea());
    }
    
    @Test
    void shapesAreSubstitutable() {
        List<Shape> shapes = Arrays.asList(
            new Rectangle(5, 10),
            new Square(5),
            new Circle(3)
        );
        
        ShapeCalculator calculator = new ShapeCalculator();
        
        // 这些方法应该能安全处理任何 Shape 组合
        double totalArea = calculator.calculateTotalArea(shapes);
        assertTrue(totalArea > 0);
        
        Shape largest = calculator.findLargest(shapes);
        assertNotNull(largest);
    }
}

4.2 示例二:使用组合替代继承

python
"""
展示如何使用组合替代继承来避免 LSP 问题
"""

from abc import ABC, abstractmethod
from typing import List


# === 不好的设计:继承用于代码复用 ===

class Duck:
    def quack(self):
        print("Quack!")
    
    def swim(self):
        print("Swimming")
    
    def fly(self):
        print("Flying")


class RubberDuck(Duck):
    """橡皮鸭不会真的叫、游或飞"""
    
    def quack(self):
        # 发出 squeak 声音而不是 quack
        print("Squeak!")
    
    def swim(self):
        # 橡皮鸭只是浮在水上
        print("Floating")
    
    def fly(self):
        # 橡皮鸭不能飞
        raise NotImplementedError("Rubber ducks can't fly")


# === 好的设计:使用组合 ===

class QuackBehavior(ABC):
    @abstractmethod
    def quack(self):
        pass


class RealQuack(QuackBehavior):
    def quack(self):
        print("Quack!")


class Squeak(QuackBehavior):
    def quack(self):
        print("Squeak!")


class MuteQuack(QuackBehavior):
    def quack(self):
        print("<< Silence >>")


class FlyBehavior(ABC):
    @abstractmethod
    def fly(self):
        pass


class RealFly(FlyBehavior):
    def fly(self):
        print("Flying")


class NoFly(FlyBehavior):
    def fly(self):
        print("Can't fly")


class SwimBehavior(ABC):
    @abstractmethod
    def swim(self):
        pass


class RealSwim(SwimBehavior):
    def swim(self):
        print("Swimming")


class Float(SwimBehavior):
    def swim(self):
        print("Floating")


class DuckV2:
    """使用组合的鸭子类"""
    
    def __init__(self, quack_behavior: QuackBehavior, 
                 fly_behavior: FlyBehavior,
                 swim_behavior: SwimBehavior):
        self.quack_behavior = quack_behavior
        self.fly_behavior = fly_behavior
        self.swim_behavior = swim_behavior
    
    def perform_quack(self):
        self.quack_behavior.quack()
    
    def perform_fly(self):
        self.fly_behavior.fly()
    
    def perform_swim(self):
        self.swim_behavior.swim()


class MallardDuck(DuckV2):
    def __init__(self):
        super().__init__(RealQuack(), RealFly(), RealSwim())


class RubberDuckV2(DuckV2):
    def __init__(self):
        super().__init__(Squeak(), NoFly(), Float())


class DecoyDuck(DuckV2):
    def __init__(self):
        super().__init__(MuteQuack(), NoFly(), Float())


# === 使用示例 ===

def main():
    ducks = [
        MallardDuck(),
        RubberDuckV2(),
        DecoyDuck()
    ]
    
    for duck in ducks:
        duck.perform_quack()
        duck.perform_fly()
        duck.perform_swim()
        print("---")


if __name__ == "__main__":
    main()

4.3 示例三:契约式设计验证 LSP

java
/**
 * 使用契约式设计确保 LSP 合规
 */

public abstract class ContractBase {
    
    /**
     * 检查前置条件
     */
    protected void checkPrecondition(boolean condition, String message) {
        if (!condition) {
            throw new PreconditionViolationException(message);
        }
    }
    
    /**
     * 检查后置条件
     */
    protected void checkPostcondition(boolean condition, String message) {
        if (!condition) {
            throw new PostconditionViolationException(message);
        }
    }
    
    /**
     * 检查不变式
     */
    protected void checkInvariant(boolean condition, String message) {
        if (!condition) {
            throw new InvariantViolationException(message);
        }
    }
}

public class PreconditionViolationException extends RuntimeException {
    public PreconditionViolationException(String message) {
        super("Precondition violated: " + message);
    }
}

public class PostconditionViolationException extends RuntimeException {
    public PostconditionViolationException(String message) {
        super("Postcondition violated: " + message);
    }
}

public class InvariantViolationException extends RuntimeException {
    public InvariantViolationException(String message) {
        super("Invariant violated: " + message);
    }
}

// === 示例:带契约的栈 ===

public class ContractStack extends ContractBase {
    
    private final List<Object> elements = new ArrayList<>();
    private final int maxSize;
    
    public ContractStack(int maxSize) {
        checkPrecondition(maxSize > 0, "Max size must be positive");
        this.maxSize = maxSize;
    }
    
    /**
     * 前置条件:栈未满
     * 后置条件:栈大小增加1,新元素在栈顶
     * 不变式:0 <= size <= maxSize
     */
    public void push(Object item) {
        int oldSize = elements.size();
        
        checkPrecondition(elements.size() < maxSize, "Stack is full");
        checkPrecondition(item != null, "Cannot push null");
        
        elements.add(item);
        
        checkPostcondition(elements.size() == oldSize + 1, "Size should increase by 1");
        checkPostcondition(elements.get(elements.size() - 1).equals(item), "New item should be on top");
        checkInvariant(elements.size() >= 0 && elements.size() <= maxSize, "Size within bounds");
    }
    
    /**
     * 前置条件:栈非空
     * 后置条件:栈大小减少1,返回原栈顶元素
     * 不变式:0 <= size <= maxSize
     */
    public Object pop() {
        int oldSize = elements.size();
        
        checkPrecondition(!elements.isEmpty(), "Stack is empty");
        
        Object result = elements.remove(elements.size() - 1);
        
        checkPostcondition(elements.size() == oldSize - 1, "Size should decrease by 1");
        checkInvariant(elements.size() >= 0 && elements.size() <= maxSize, "Size within bounds");
        
        return result;
    }
    
    public int size() {
        return elements.size();
    }
    
    public boolean isEmpty() {
        return elements.isEmpty();
    }
}

// === 子类必须遵守相同的契约 ===

public class LoggingStack extends ContractStack {
    
    private final Logger logger;
    
    public LoggingStack(int maxSize, Logger logger) {
        super(maxSize);
        this.logger = logger;
    }
    
    @Override
    public void push(Object item) {
        logger.info("Pushing: " + item);
        super.push(item);
        logger.info("Pushed. Size: " + size());
    }
    
    @Override
    public Object pop() {
        logger.info("Popping. Current size: " + size());
        Object result = super.pop();
        logger.info("Popped: " + result);
        return result;
    }
}

// LoggingStack 没有改变前置条件、后置条件或不变式,符合 LSP

五、深入思考

5.1 历史背景

LSP 由 Barbara Liskov 在 1987 年的论文"Data Abstraction and Hierarchy"中首次提出。1994 年,她与 Jeannette Wing 合作发表了更形式化的定义。

Barbara Liskov 是计算机科学领域的先驱女性,2008 年因在编程语言和系统设计的实践与理论方面的贡献获得图灵奖。

Robert C. Martin 将 LSP 引入到 SOLID 原则体系中,使其成为面向对象设计的重要指导原则。

5.2 前沿方向

在现代软件工程中,LSP 的应用正在演变:

类型系统增强:Kotlin、Rust 等现代语言的类型系统可以更好地表达子类型关系。

契约测试:在微服务架构中,消费者驱动的契约测试确保服务提供者不破坏契约。

形式化验证:使用定理证明工具验证代码是否符合契约。

5.3 延伸阅读

  • 《Object-Oriented Software Construction》- Bertrand Meyer:契约式设计的权威著作
  • 《Design by Contract》- Eiffel 软件的方法论
  • 《Refactoring》- Martin Fowler:用组合替换继承的重构手法

六、实验笔记

实验一:识别和修复 LSP 违反

目标:在一个真实项目中识别违反 LSP 的继承关系并修复。

步骤:

选择一个项目,找出所有继承关系

对每个继承关系,检查子类是否可以完全替代父类

寻找 instanceof 检查、UnsupportedOperationException 等信号

修复违反 LSP 的设计,使用组合或重新建模

验证所有测试通过

实验二:设计符合 LSP 的类型层次

目标:设计一个动物类型层次,确保符合 LSP。

任务:考虑各种动物(鸟、企鹅、鸵鸟、鱼、哺乳动物等)及其能力(飞、游泳、行走等)。


七、本章小结

本章深入探讨了里氏替换原则(LSP),这是关于子类型行为一致性的原则。我们学习了:

LSP 要求子类能够替换父类而不改变程序的正确性。这不仅是类型兼容性问题,更是行为契约问题。

契约式设计提供了验证 LSP 的框架:子类不能加强前置条件、不能削弱后置条件、不能破坏不变式。

经典的"正方形-矩形问题"展示了名义上的"is-a"关系不一定意味着行为上的可替换性。

组合优于继承是避免 LSP 问题的重要策略。通过将行为封装为独立的对象并使用组合,可以更灵活地构建功能。

LSP 是多态正确性的保证,也是 OCP 和 DIP 的前提条件。


八、习题

概念理解:解释为什么 LSP 不仅仅是类型兼容性问题,而是行为契约问题。

案例分析:分析以下代码是否违反 LSP,并说明理由:

java
class Bird {
    void layEggs() { /* ... */ }
}
class Bat extends Bird {
    @Override
    void layEggs() {
        throw new UnsupportedOperationException("Bats don't lay eggs");
    }
}

设计练习:设计一个交通工具类型层次,包括汽车、自行车、飞机、船等,确保符合 LSP。

重构实践:将一个使用继承的代码重构为使用组合。比较两种设计的优缺点。

契约设计:为一个队列类设计前置条件、后置条件和不变式。

研究题:调研 Kotlin 或 Rust 的类型系统如何处理子类型关系。

批判性思考:在什么情况下,轻微违反 LSP 可能是可以接受的?给出具体场景。

测试关联:如何编写测试来验证子类符合 LSP?

多态与安全:解释 LSP 如何保证多态的安全性。

实际应用:在你熟悉的项目中,找出一个违反 LSP 的例子,并提出改进方案。

5/12里氏替换
0%