我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
假设我们在代码中使用了一个矩形
r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);
在几何课上,我们学过正方形是一种特殊类型的矩形,因为它的长宽相等。让我们根据下面的信息创建一个Square类:
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
如果我们在第一个代码中将矩形替换为正方形,那么它将会中断:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
这是因为正方形有一个我们在矩形类中没有的新前提条件:width == height。根据LSP,矩形实例应该被矩形子类实例替代。这是因为这些实例通过了矩形实例的类型检查,因此它们将在代码中导致意外错误。
这是wiki文章中“在子类型中不能加强先决条件”部分的一个例子。因此,总而言之,违反LSP可能会在某些时候导致代码错误。
其他回答
以Board数组的形式实现ThreeDBoard会有用吗?
也许你想把不同平面上的ThreeDBoard切片作为一个板。在这种情况下,您可能希望为Board抽象出一个接口(或抽象类),以允许多种实现。
就外部接口而言,您可能希望为TwoDBoard和ThreeDBoard提取一个Board接口(尽管上述方法都不适合)。
它指出,如果C是E的子类型,则E可以替换为C类型的对象,而不会改变或破坏程序的行为。简单地说,派生类应该可以替代它们的父类。例如,如果一个农民的儿子是农民,那么他可以代替他的父亲工作,但如果一个农民的儿子是板球运动员,那么他就不能代替他的父亲工作。
违反的例子:
public class Plane{
public void startEngine(){}
}
public class FighterJet extends Plane{}
public class PaperPlane extends Plane{}
在给定的例子中,fighter和PaperPlane类都扩展了包含startEngine()方法的Plane类。所以很明显,战斗机可以启动引擎,但纸飞机不能,所以它破坏LSP。
PaperPlane类虽然扩展了Plane类,但应该可以替代Plane类,但它不是Plane实例可以被替换的合格实体,因为纸飞机不能启动引擎,因为它没有引擎。好的例子是,
受人尊敬的例子:
public class Plane{
}
public class RealPlane{
public void startEngine(){}
}
public class FighterJet extends RealPlane{}
public class PaperPlane extends Plane{}
可替代性是面向对象编程中的一个原则,它指出,在计算机程序中,如果S是T的子类型,那么类型T的对象可以被类型S的对象替换
让我们用Java做一个简单的例子:
不好的例子
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
鸭子能飞,因为它是鸟,但这个呢:
public class Ostrich extends Bird{}
鸵鸟是一种鸟,但它不能飞,鸵鸟类是鸟类的一个子类,但它不应该能够使用fly方法,这意味着我们打破了LSP原则。
很好的例子
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
在一个非常简单的句子中,我们可以说:
子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。
Liskov替换原理(LSP, LSP)是面向对象编程中的一个概念,它指出:
函数使用指针或 基类的引用必须是 能够使用派生类的对象 在不知不觉中。
LSP的核心是关于接口和契约,以及如何决定何时扩展一个类,还是使用另一种策略(如组合)来实现您的目标。
我所见过的说明这一点的最有效的方法是《Head First OOA&D》。它们呈现的场景是,你是一名致力于为策略游戏构建框架的项目开发者。
他们展示了一个类,它代表一个板子,看起来像这样:
所有的方法都以X和Y坐标作为参数来定位tile在二维tile数组中的位置。这将允许游戏开发者在游戏过程中管理棋盘上的单位。
这本书继续改变了要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏。因此引入了一个ThreeDBoard类,它扩展了Board。
乍一看,这似乎是个不错的决定。Board提供了高度和宽度属性,ThreeDBoard提供了Z轴。
当你看到从董事会继承的所有其他成员时,它就失效了。AddUnit, GetTile, GetUnits等方法在Board类中都采用X和Y参数,但ThreeDBoard也需要Z参数。
因此,您必须使用Z参数再次实现这些方法。Z参数没有Board类的上下文,从Board类继承的方法失去了意义。试图使用ThreeDBoard类作为其基类Board的代码单元将非常不走运。
也许我们应该另想办法。ThreeDBoard应该由Board对象组成,而不是扩展Board。Z轴上每单位一个板子对象。
这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP。
推荐文章
- 合并两个PHP对象的最佳方法是什么?
- 继承和组合的区别
- 如何在PostgreSQL中查看视图的CREATE VIEW代码?
- 打印Python类的所有属性
- 面向对象编程,函数式编程,过程式编程
- 面向对象的Javascript最佳实践?
- 为什么我更喜欢使用成员初始化列表?
- 如何让PHP类构造函数调用父类的父类构造函数?
- 理解__getattr__和__getattribute__之间的区别
- 让setter返回"this"是不好的做法吗?
- JavaScript中的类与静态方法
- 聚合、组合和依赖之间的区别是什么?
- Javascript是基于原型的语言,这意味着什么?
- 在类方法上使用property()
- 子类继承私有字段吗?