里氏替换原则指出:“继承必须确保超类所拥有的性质在子类中仍然成立”,在程序中的表现就是某个接口能接受超类对象为参数,那么它也必须应该能接受子类对象为参数,且程序不会出现异常。也就是说子类对象应该能够替换掉超类对象,而程序的行为不会改变。

最经典的用于说明里氏替换原则的反例就是“正方形不是长方形”。

假设我们有一个 Rectangle 类,它有 width 和 height 两个属性,以及它们的 getter 和 setter 方法,还有一个 area 方法用于求矩形的面积。

class Rectangle {
public:
    virtual void setWidth(int w) {
        width = w;
    }
    virtual void setHeight(int h) {
        height = h;
    }
    int getWidth() const {
        return width;
    }
    int getHeight() const {
        return height;
    }
    virtual int area() const {
        return width * height;
    }
protected:
    int width;
    int height;
};

这里的 width、height 需要设置为 protected,否则继承后将无法访问这两个属性。

然后我们创建一个 Square 类,它继承了 Rectangle 类,因为它们的长和高总是相等的,所以我们要重写 width 和 height 的 setter方法。

class Square : public Rectangle {
public:
    void setWidth(int w) override {
        width = height = w;
    }
    void setHeight(int h) override {
        width = height = h;
    }
};

然后现在有个接口,它接受 Rectangle 对象的引用作为参数,并设置长和宽,然后调用 area 并设置断言判断与预期是否一致。

void process(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(4);
    assert(r.area() == 20); // 当 r 为 Square 时断言错误。
}

这里的断言在 r 为 Rectangle 时会成功,而 r 为 Square 时会失败。

int main() {
    Rectangle r;
    process(r); // 成功

    Square s;
    process(s); // 失败

    return 0;
}

很明显,在接口 process 中 Square 不能替换 Rectangle,因为当 Square 替换 Rectangle 作为参数时,程序发生了异常,出现了预期之外的结果。而最根本的原因是 Square 不能继承 Rectangle 中,因为 Rectangle 的属性 width 和 height 并不全是 Square 应该拥有的属性,或者说 Square 不应该拥有两个独立的属性,而应该拥有单一的边长属性 side。

所以,为了确保里氏替换原则成立,我们应该取消 Square 对 Rectangle的继承,重新给 Square 和 Rectangle 设计一个更高层的抽象,如 Shape,Shape 中有一个 Square 和 Rectangle 共有的属性 area,然后让 Square 和 Rectangle 都继承 Shape。

抽象类 Shape:

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

Rectangle 继承 Shape 重写 area 接口并定义自己独特的成员变量 width 和 height 以及对应的 setter:

class Rectangle : public Shape {
public:
    void setWidth(int w) {
        width = w;
    }
    void setHeight(int h) {
        height = h;
    }
    int area() const override {
        return width * height;
    }
private:
    int width;
    int height;
};

Square 继承 Shape 重写 area 接口并定义自己独特的成员变量 side 以及对应的 setter:

class Square : public Shape {
public:
    void setSide(int s) {
        side = s;
    }
    int area() const override {
        return side * side;
    }
private:
    int side;
};

接口 process 接收超类 Shape 作为参数:

void process(Shape& s) {
    std::cout << "Area: " << s.area() << std::endl;
}

在 main 函数中 process 分别接受 Rectangle 和 Square 类型对象:

int main() {
    Rectangle r;
    r.setWidth(5);
    r.setHeight(4);
    process(r); // 成功

    Square s;
    s.setSide(5);
    process(s); // 成功

    return 0;
}

运行程序后发现,无论是 Rectangle 还是 Square 类型对象 process 接口均能正常处理并且没有出现异常,也就意味着 Rectangle 和 Square 两个子类能替换掉超类并且程序行为没有改变,也就说明这次的继承关系和接口设计符合里氏替换原则。

以上就是本文的全部内容,需要完整可运行代码请查看 Github 仓库GnCDesignPatterns

本站无任何商业行为
个人在线分享 » 里氏替换原则经典反例:正方形不是长方形
E-->