我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
利科夫替换原则指出,如果程序模块使用基类,则基类的引用可以被派生类替换,而不会影响程序模块的功能。
派生类型必须能够完全替代它们的基类型。
示例- java中的协变返回类型。
其他回答
以Board数组的形式实现ThreeDBoard会有用吗?
也许你想把不同平面上的ThreeDBoard切片作为一个板。在这种情况下,您可能希望为Board抽象出一个接口(或抽象类),以允许多种实现。
就外部接口而言,您可能希望为TwoDBoard和ThreeDBoard提取一个Board接口(尽管上述方法都不适合)。
该原则由Barbara Liskov在1987年提出,并通过关注超类及其子类型的行为来扩展开闭原则。
当我们考虑违反它的后果时,它的重要性就变得显而易见了。考虑一个使用以下类的应用程序。
public class Rectangle
{
private double width;
private double height;
public double Width
{
get
{
return width;
}
set
{
width = value;
}
}
public double Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
想象一下,有一天,客户要求除了矩形之外还能操作正方形。因为正方形是矩形,所以square类应该派生自rectangle类。
public class Square : Rectangle
{
}
然而,这样做会遇到两个问题:
一个正方形不需要从矩形继承高度和宽度变量,如果我们必须创建成千上万个正方形对象,这可能会造成严重的内存浪费。 从矩形继承的width和height setter属性不适用于正方形,因为正方形的宽度和高度是相同的。 为了将height和width设置为相同的值,我们可以创建两个新属性,如下所示:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
现在,当有人设置一个正方形物体的宽度时,它的高度将相应地改变,反之亦然。
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
让我们继续考虑另一个函数:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
如果我们将一个方形对象的引用传递给这个函数,我们将违反LSP,因为该函数对其参数的导数不起作用。属性width和height不是多态的,因为它们在矩形中没有被声明为虚的(正方形对象将被损坏,因为高度不会被改变)。
然而,通过将setter属性声明为virtual,我们将面临另一个违反,即OCP。事实上,派生类正方形的创建会导致基类矩形的变化。
LSP说“对象应该被它们的子类型替换”。 另一方面,这一原则指向
子类永远不应该破坏父类的类型定义。
通过以下示例,可以更好地理解LSP。
没有太阳能发电:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
return; //it isn`t rendered in this case
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
LSP修复:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
showAd();//it has a specific behavior based on its requirement
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
它指出,如果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{}
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()
- 子类继承私有字段吗?