专栏首页敏捷教练简单聊聊契约式设计(上)

简单聊聊契约式设计(上)

我在阅读Bob大叔的《敏捷软件开发:原则、模式与实践》第十章的时候第一次接触Design by Contract这个概念。Bob大叔在讲述面向对象设计SOLID原则中的LSP(Liskov Substitution Principle)时,就借助DbC的设计思想来支撑LSP[1]。关于DbC,我将用两篇文章来简单聊聊。

袁帅大学毕业后半年左右折回到长安大学的IT部门。他被安排到一个刚启动的教学软件系统项目。系统中已经存在一个长方形(Rectangle)类,该类有设置宽、高以及计算面积的行为:

public class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double width) {
        this.width = width;
    }
    public void setHeight(double height) {
        this.height = height;
    }
    public double calculateArea(){
        return width * height;
    }
}

入职后第二周,接到新需求要实现一个正方形(Square)。周一,袁帅开完站会后就Kick off了这张卡。看着行数不多的代码库,他思忖着:”A Square is a Rectangle,这个小学数学教了多少遍的概念一定不会有错”。于是,他果断创建了一个Square类,并继承Rectangle程序功能要满足客户需求这条准则袁帅还是没有忘记的,他心里清楚得覆写设置宽和高的方法:

public class Square extends Rectangle {
    @Override
    public void setHeight(double height) {
        this.height = height;
        this.width = height;
    }
    @Override
    public void setWidth(double width) {
        this.height = width;
        this.width = width;
    }
}

接着,袁帅将用户使用场景翻译成如下代码:

public class Client {
    public static void main(String[] args) {
        assertStandardHouseArea(new Rectangle());
    }
    public static void assertStandardHouseArea(Rectangle rectangle) {
        rectangle.setHeight(20);
        rectangle.setWidth(30);
        assert rectangle.calculateArea() == 600;
    }
}

不到一上午时间,他就提前完成了功能开发,午饭时间还没到,他边等时间边琢磨怎么折腾一下他这个代码,争取不给QA留下”把柄”。想着想着,他回忆起大二书上看到的一个概念 – LSP,模模糊糊记得是 “子类能够替换掉父类…“。既然是子类、父类,不就是继承关系吗。于是他模拟用户使用时传入了一个子类对象:

public class Client {
    public static void main(String[] args) {
        assertStandardHouseArea(new Square()); // Failed
    }
}

程序挂了,一试就中,袁帅小有成就感。因为不太确定LSP完整含义,他顺手翻开桌前的《敏捷软件开发:原则、模式与实践》,快速目录锁定到LSP的位置。LSP – 子类对象能够替换掉父类对象,而且不会引发程序的不一致。

他没有怀疑古人提出的LSP是否本身有问题,而是先反观自己的程序:”得是哪里不对劲!”。查看了15分钟,没看出明显问题,百思不得其解,他只好继续往后阅读。

突然,他好像明白了点什么 – 原来Rectangle/Square继承结构对assertStandardHouseArea的使用者来说是脆弱的。

假设将RectangleSquare模型独立看,各自的使用者只知道RectangleSquare,分别使用这两个模型的时候不会存在这个问题,这两个孤立的模型都是有效的。一旦这两个模型发生了继承关系,相当于组合后构建了一个新的模型,但是对于使用者来说,他的期望是建立在父类Rectangle之上的,而Square继承了父类后,又打破了这个期望,这个新的模型对于用户来说就失效了。

似懂非懂,袁帅对模型的有效性有了新的疑问,带着好奇心,继续往下读。

什么是模型的有效性?

LSP则得出的重要结论:一个模型,如果孤立地看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。

看到上面这句话,袁帅尝试回到自己的代码去理解。当孤立看RectangleSquare时,它们各自都是有效的,为何有效?因为这两个模型的使用者分别有如下假设:

  1. Rectangle,长和宽独自变化,互不影响。
  2. Square,长和宽是同时变化,且始终相等。

Square继承Rectangle之后,使用者的假设就变了:

  1. Rectangle,长和宽独自变化,互不影响。
  2. Square is a Rectangle,回到假设1。

很明显,Square覆写了设置宽和高的方法后,破坏了用户对父类Rectangle的假设。

袁帅平时喜欢看武侠小说,他盯着屏幕的代码,思绪飘到了江湖:

一江湖侠客(用户)经常使用的一把宝剑,出鞘进鞘如行云流水。另一刺客(用户)手持利刃,刀光剑影不出三招必拿下人头,快到你以为刀未曾出鞘。本来这两人,使用自己的武器非常顺手(独立看模型,都没问题)。此时,调皮的袁帅,趁侠客舞剑,把刺客的刀鞘插到侠客的剑桥中(好比胡乱继承),侠客舞剑完毕按照老习惯将剑入鞘(原有假设),很可能会非常尴尬(程序出错)。

image

他拿起了笔,在纸上画下刚才几个模型:

  1. 宝剑 + 剑鞘
  2. 利刃 + 刀鞘
  3. 宝剑 + 剑鞘 + 刀鞘 + 利刃

孤立去看宝剑 + 剑鞘 以及利刃 + 刀鞘这两个模型,各自依然有效成立的。将剑鞘和刀鞘结合之后,侠客的宝剑就没法入鞘。

袁帅突然回过神来,注意到合理假设这个词,喜欢思考的他又产生了一个新的问题 – “那我怎么知道用户会做出哪些合理假设呢?”

此时,他看到书中Bob大叔提到一个观点:

如果我们试图去预测所有的假设,代码很可能会充斥着浓浓的味道。我们应该优先预测那些明显违背了LSP的设计,延迟其他的预测,直到出现了脆弱性的臭味时。

读到这里,他心中能明确的点是 – 那些明显违背了LSP的设计是不好的,应该当心警惕,并且予以及时修正。对于这个结论,他没有想要去驳斥,但他也不甘心做第六只猴子,只知道遵守,不知道为什么遵守?

为什么继承之后,模型就失效了?

袁帅看了眼手机,已经下午1点,错过了午饭黄金时间,他觉得没有必要再去食堂吃饭了,于是手机点了个外卖,开始总结刚才学习到的内容。

正方形是一个矩形,这个在现实世界中极其合理的关系。而在OO软件设计中,IS-A针对的是对象的行为而言。使用者会对对象的行为作出合理假设,而且是基于父类的行为做出的假设,如果子类的行为跟父类的的行为不兼容,就要当心这个继承的隐患。

基于此,袁帅在笔记上给LSP记了如下重点:

  1. 对象的行为方式才是软件真正所关注的问题。
  2. 行为方式是可以进行合理假设的,它是客户程序所依赖的。
  3. 在OOD中,IS-A的关系是就行为而言的[2]。

如何明确用户的合理假设?

趁外卖还没有送到,袁帅又陷入了沉思:”基于用户的合理假设来审视我们的模型设计,就需要我去猜测用户会存在哪些合理假设,而这种猜测总会让人觉得心里不踏实,到底如何才能知道客户的真正要求呢?”

突然手机铃声响了:”袁先生你好,你的外卖到了,麻烦请到E座楼下取一下。”

参考阅读

  1. 听面向对象先生聊SOLID创业故事
  2. 聊聊面向对象设计中的Is-A

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 聊聊面向对象设计中的Is-A

    面向对象编程范式得到了广大开发者的青睐,在做面向对象软件设计的同仁也或多或少曾经心存困惑过。比如,怎么样才是正确的封装?如何恰当的继承?何时应该抽象? 对于设计...

    袁慎建@ThoughtWorks
  • 从Task到Test,语言不统一会怎么样?

    自从DDD被重新拉回到人们的视线之后,统一语言这个术语也越来越多得出现在不同的场合。尤其是业务人员与技术人员在沟通的时候,业务人员听不懂技术人员在讲什么,技术人...

    袁慎建@ThoughtWorks
  • Scrum需要一个双刃团队

    1993年,Jeff和Ken开创了Scrum,至今已经有25年之久。如今敏捷开发也不是什么流行词儿,不少IT组织已经走在敏捷转型的路上,还有一部分组织则刚痛下决...

    袁慎建@ThoughtWorks
  • Actframework依赖注入 II - 注入对象类型

    老码农
  • annotationProcessor 自动生成代码(上)

    有时候,我们需要开发大量重复的代码。每段代码,只有少数成员变量命名不同。这样的场景在开发接口层时,感觉尤为明显。 接口类可能只是实现类的抽象形式。但每个实现方...

    Oceanlong
  • 【原创】JVM系列04 | 栈上分配

    当面试官问你对象都分配哪里,你把 JVM 内存结构介绍一下然后说分配在堆上,没啥问题,给你打 8 分。如果你还能聊一聊栈上分配,一定是加分项,我想面试官会考虑给...

    java进阶架构师
  • 在vue中使用stylus的混入

    用户4344670
  • idea新建springCloud项目(4)- 商品服务

    将application.properties修改为application.yml

    MonroeCode
  • 四、原型模式与建造者模式详解

    原型模式(PrototypePattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。

    编程之心
  • Android插件化基础3----Android的编译打包流程详解

    .apk文件其实就是一个压缩包,把文件的后缀改成.zip,用压缩软件解压搜就可的下图(我是mac)

    隔壁老李头

扫码关注云+社区

领取腾讯云代金券