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