里氏替换原则指出:“继承必须确保超类所拥有的性质在子类中仍然成立”,在程序中的表现就是某个接口能接受超类对象为参数,那么它也必须应该能接受子类对象为参数,且程序不会出现异常。也就是说子类对象应该能够替换掉超类对象,而程序的行为不会改变。
最经典的用于说明里氏替换原则的反例就是“正方形不是长方形”。
假设我们有一个 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