软件设计原则之里氏替换原则
定义
里氏替换原则(Liskov Substitution Principle, LSP)是 SOLID 原则之一,它指出:子类型必须能够替换它们的基类型。换句话说,在使用基类的地方应当可以透明地使用派生类的对象而不会影响程序的正确性。
违反里氏替换原则的一个典型反例是正方形(Square)和矩形(Rectangle)的关系。在几何学中,正方形是一种特殊的矩形,其中所有边等长。然而,如果我们在面向对象编程中直接继承关系来实现这一点,可能会违反 LSP。
反例
class Rectangle {
protected int width = 0;
protected int height = 0;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 违背了 LSP,因为正方形的宽高需要同步设置
}
@Override
public void setHeight(int height) {
super.setWidth(height); // 同样违背了 LSP
super.setHeight(height);
}
}
问题
如果我们试图使用 Rectangle
类型的引用指向 Square
实例,并尝试改变其宽度或高度,我们会发现行为不符合预期:
public static void main(String[] args) {
Rectangle rect = new Square(); // 正常情况下期望能这样做
rect.setWidth(5);
rect.setHeight(4);
System.out.println(rect.getArea()); // 如果 Square 继承自 Rectangle,这可能输出 25 而不是预期的 20
}
在这个例子中,Square
类的行为与 Rectangle
类不一致,导致了违反 LSP 的情况发生。这是因为 Square
强制要求宽度和高度保持一致,而 Rectangle
则允许它们独立变化。
如何修正
为了解决这个问题,我们需要重新考虑设计。一种方法是避免让 Square
直接继承自 Rectangle
,而是将它们视为两个独立但相关的类,或者通过组合而非继承来表达它们之间的关系。例如,可以通过定义接口来描述两者共有的行为,而不依赖于具体的实现细节。
正确的做法可能是确保任何使用 Rectangle
的地方都能够安全地接受 Rectangle
的任何子类,而不改变程序的正确性和预期行为。这样就遵循了里氏替换原则。
修正
设计接口
// 矩形接口 求面积 周长等
public interface IRectangle {
int area();
}
面向对象 设计1 使用组合 分离Square
矩形类
@AllArgsConstructor
@Data
public class Rectangle1 implements IRectangle {
protected int width = 0;
protected int height = 0;
@Override
public int area() {
return width * height;
}
}
正方形类
public class Square1 implements IRectangle {
private int edge;
public Square1(Rectangle rectangle) {
if (rectangle.width != rectangle.height) {
throw new RuntimeException("不是正方形,长方形必须长宽相等");
}
this.edge = rectangle.getWidth();
}
@Override
public int area() {
return edge * edge;
}
}
面向对象 设计2 使用继承 添加特殊子类标记完善矩形类
问题;这里扩充父类,会违反单一职责原则吗?
如果一个对象承担了太多的职责,至少存在以下两个缺点:
1、一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
2、当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
我认为是没有违反单一职责原则的。比如再有一个长方形类,长方形会继承矩形判断是否是正方形的属性和方法,当长方形当做矩形(多态、依赖倒置)使用时是可以有这个判断条件的,因为矩形本身就含有是否是正方形这个功能。
矩形类
@Data
public class Rectangle implements IRectangle {
protected int width = 0;
protected int height = 0;
protected boolean isSquare;
public Rectangle() {
this(false);
}
public Rectangle(boolean isSquare) {
this.isSquare = isSquare;
}
public void setHeight(int height) {
if (isSquare) {
this.width = height;
this.height = height;
} else {
this.height = height;
}
}
public void setWidth(int width) {
if (isSquare) {
this.width = width;
this.height = width;
} else {
this.width = width;
}
}
public int area() {
return width * height;
}
public boolean isSquare() {
return isSquare;
}
}
正方形类
public class Square extends Rectangle {
public Square(Rectangle rectangle) {
if (rectangle.width != rectangle.height) {
throw new RuntimeException("不是正方形,必须长宽相等");
}
}
public Square(boolean isSquare) {
super(isSquare);
}
}
测试
public class Client {
public static void main(String[] args) {
// 使用组合
Rectangle1 rectangle1 = new Rectangle1(5, 4);
Square1 square = new Square1(rectangle1);
System.out.println(square.area());
// 使用继承 添加特殊子类的标记
Rectangle rectangle = new Square(true);
rectangle.setHeight(5);
rectangle.setWidth(4);
System.out.println(rectangle.area());
}
}
总结
里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士 在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy) 里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。
关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。