LSP和合同编程

问候,

本周的技巧介绍了有关面向对象的一些理论

编程。 首先,我们引入一个示例项目:一个简单的几何

项目。 我们要定义一堆代表可调整大小的形状的类。

没什么复杂的:我们定义一个抽象的Shape类,然后开始:


public abstract class Shape { 
   abstract public double getArea();
   ... 
}
public class Rectangle extends Shape {
   // it's dimensions
   private double width, height;
   // contructor
   public Rectangle(double width, double height) {
      this.width= width;
      this.height= height;
   }
   // getters and setters:
   public double getWidth() { return width; }
   public double getHeight() { return height; }
   public void setWidth(double width) { this.width= width; }
   public void setHeight(double height) { this.height= height; }
   // get the area
   public double getArea() { return width*height; }
}
抽象形状类不知道如何计算面积,因此该方法

被抽象化。 矩形定义此方法。 因为矩形*是*

形状,我们从抽象类的形状扩展。 这个小场景可以是

几乎可以在任何初学者的教科书中找到; 看着这几乎无聊

在最紧急的时间。

我们通过定义一个Square完成这个无聊的小项目。 众所周知,广场

* is-a *是Rectangle的特殊类型,因此我们扩展了Rectangle类:


public class Square extends Rectangle {
   // constructor
   public Square(double side) { super(side, side); }
   // getters and setters:
   public double getSide() { return getWidth(); } // or getHeight()
   public void setSide(double side) {
      super.setWidth(side);
      super.setHeight(side);
   }
   public void setWidth(double width) { this.setSide(side); }
   public void setHeight(double width) { this.setSide(side); }
}
在那里:完成。 我们使用了大多数Rectangle代码:我们甚至不必

定义一个新的getArea()方法,一切都很好。 我们把所有东西都包好

构建一个.jar文件,将其刻录到CD上(我们甚至在其上贴上一个漂亮的标签),然后发送

CD和账单给我们的客户,我们将享受周末。

当天,我们的客户收到CD,安装了所有东西,

因为他是一个讨厌的nitpicker,所以他想测试一下:


public static void main(String[] args) {
   Rectangle r= new Rectangle(1.0, 0.0);
   for (int i= 1; i <= 10; i++) {
      r.setHeight(i);
      if (r.getArea() != i)
         System.err.println("This software is not correct!);
   }
}
那天晚些时候,我们的客户给我们打了个电话,告诉我们测试已经

到目前为止还算不错。 我们知道我们的软件很好,我们向我们表示祝贺

客户使用他的新的,正确的正确软件。

我们的客户还知道Square * is-a *矩形的特殊类型,因此他

制作他的下一个测试:


public static void main(String[] args) {
   Rectangle r= new Square(1.0);
   for (int i= 1; i <= 10; i++) {
      r.setHeight(i);
      if (r.getArea() != i)
         System.err.println("This software is not correct!);
   }
}
你猜怎么了? 当天晚些时候,我们的客户再次致电:他很生气; 九分

十次测试运行失败; 只是单元平方测试成功。 他所做的只是

实例化一个Square而不是Rectangle,并且因为Square * is-a *

矩形应该可以正常工作,但不能正常工作。

我们决定暂时跳过该帐单,然后再看看我们的

类; 周末去了。

在午夜到来之前,我们意识到我们走错了路:

无论我们如何保持宽度和高度都相等,

如果我们认为Square是特殊的,则getArea()会给出意外结果

矩形。 我们也不能这样做:


public class Square extends Rectangle {
   ...
   public void setWidth(double width) {
      throw IllegalArgumentException("Don't use this method for Squares!");
   }
   public void setHeight(double height) {
      throw IllegalArgumentException("Don't use this method for Squares!");
   }
}
这种黑客肯定不会通过我们客户的测试套件。 但是一个正方形

*是*一种特殊的矩形。 还是?

1988年,巴拉巴拉·利斯科夫(Barabara Liskov)对这个问题进行了以下观察:

“这里需要的是类似以下替换属性的内容:
如果对于类型D的每个对象o2,都有一个类型B的对象o1,使得
所有程序P均以B定义,当o2时P的行为不变
被o1取代,那么D可以是B的子类型。”

-数据抽象和层次结构,SIGPLAN通告,23,5(1988年5月)。

巴拉巴拉·里斯科夫(Barabara Liskov)是数学类型,数学家喜欢

希腊文:您给他们一个问题,他们将其翻译为自己的语言

突然之间,您不再了解自己的问题。 (*)

对于可能穿网球鞋或偶尔穿Python皮靴子的普通人来说,

她这样说:程序P是我们客户的测试套件(请参见上文)。 o1

object是Rectangle类中的对象。 我们客户的测试套件有效

和他们在一起很好(还记得第一个快乐的电话吗?)

是派生类(来自类D的对象o2,在我们的示例中为正方形)

该程序应该可以继续正常运行。

这一切都很好,但我们的Square类(扩展了Rectangle)

没有通过此替代测试。 这就是巴拉巴拉·里斯科夫(Barabara Liskov)的LSP

都是关于

:如果派生类未通过此测试,则不应为

派生类。

但是再说一遍:正方形* is-a *矩形。 从数学上讲是的,

但在我们可以调整这些内容大小的示例中,“正方形”不是“矩形”。

* is-a *关系实际上不是数学关系。 这是一种行为

关系和我们的尝试在LSP测试驱动器中惨败。

该程序P期望Rectangle继续运行,无论是否

它们是“特殊”矩形。 如果在矩形上运行正常,则应该运行良好

一个特殊的矩形。 如果运行不正常,则特殊的Rectangle不是

应该完全是Rectangle,而不应该是

矩形类。 这就是LSP的全部意义。 这是一种石蕊测试

对于您的继承树:如果子类无法充当其父类:

不要让班级成为那个父母的孩子。 (通常不起作用

在现实世界中的方式;-)

最后,这是我们重新设计的Square类:


public class Square extends Shape {
   private double side;
   // constructor:
   public Square(double side) { this.side= side; }
   // getters and setters: 
   public double getSide() { return side; }
   public void setSide(double side) { this.side= side; }
   // the area implementation:
   public double getArea() { return side*side; }
}
是无聊的吗? 但是现在它通过了LSP!

还剩下一件事:芭芭拉·利斯科夫(Barbara Liskov)为什么写“那么它可以是……

子类”。Java的答案很简单:如果两个类实现相同的

接口,并且程序P需要该接口,它应该运行良好

无论同一个的不同实现(类)中的哪个

接口被传递到该程序P。

LSP制定后不久,Bertrand Meyers提出了

与LSP接壤的“按合同设计”概念。 每种方法都有

前提条件和后置条件。 调用方法时,其前提条件

必须持有; 方法完成时,它承诺其后置条件成立。

例如,Rectangle中setWidth方法的后置条件

类,是:

this.width ==宽度&& this.heigth == oldHeight

其中oldHeight表示调用该方法之前的高度值。

正如Meyers所说:

“重写方法(在派生类中)时,您只能替换
以弱者为前提的先决条件,以强者为条件替换后置条件
“将超类方法的条件”

如果超类中的“更强”前提,则“更弱”前提为真

是真的。 换句话说,当通过基本类型使用对象时,用户

(LSP中的程序P)仅知道...的先决条件和后置条件。

基类。 因此,派生对象一定不能期望此类用户服从

前提条件要比基类要求的条件强。 那是,

他们必须接受基类可以接受的任何东西。 另外,派生

类必须符合基础的所有后置条件,即它们的行为

并且输出(副作用)不得违反基类施加的约束。

可以清楚地看到,矩形/正方形示例违反了“更强

Square条件中的后置条件”,即上面显示的后置条件

矩形类在派生的Square类中不成立。

直到下一次将是“播放时间”:数独解算器及所有其他功能。

亲切的问候,

乔斯

From: https://bytes.com/topic/java/insights/642579-lsp-programming-contract

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值