在上一篇文章中通过里氏替换原则的示例,Bob大叔抛出了一个观点 – 做模型设计的时候,要基于客户程序使用的角度去审视模型的有效性。这就需要我们要去猜测客户程序的一些”合理”的假设。当一个事情需要靠猜测的时候,我们总会觉得心里不安。Bob大叔提到DbC这项技术,能够帮助我们来明确用户的合理假设。本文来聊聊如何借助DbC的设计思想来加持LSP。

契约促进社会秩序稳定

快速摊开还热乎外卖套餐,袁帅的思绪又回到了”用户的合理假设?”,即便目光聚焦在Bob大叔的书上,也不影响他机械地咀嚼着米饭。突然,眼前飘过DbC(Design by Contract)这个概念。他如获至宝般两眼发光,扔下筷子,开始逐字逐句研读书中语句,有一副要把文字当菜吃掉的架势。

按契约式设计,字面上看起来,袁帅觉得没什么特别的。而且契约这个词,他也没少听说,人与人之间的契约,软件开发中的契约测试,守信用也是一种契约守护。但他不确定这个DbC是否表达这个意思。为求甚解,他去翻阅了一些资料:

契约是指”依照法律订立的正式的证明.出卖.抵押.租赁等关系的文书”。 – 《现代汉语词典》

很久前,我们的祖先就发明了契约。古代以物易物,我用2斤大米跟你换1斤黄豆,我今天帮你干半天活,你明天帮我干半天活。再到后来,发明了货币制度,用货币去购买物品,背后其实体现的也是一种契约精神。

由于上述的交易很快完成,不需要正式的合同文书。对于那些具有持续义务的交易,比如按揭买房,就需要通法律的保障来强化契约,所以就有了后来”加盖公章”协议和合同。

掌握了这些信息后,袁帅心里很清楚:契约明确地规定了双方履行的职责,以及各自享受的权益。在现实生活中的房产交易,一旦有一方变卦违约了,另一方就会受到法律保护。

他在想:”在软件设计中,我们也能这么幸运吗?我设计的API接口,如果使用者不按照我的要求使用,或者使用者按照我的规则来使用,却得到了意外的结果。这样的事情,总会让人不开心。那能否也引入契约精神,来减少这种没必要的意外呢?”

此时,他隐约地感知到Bob大叔在书中提到的DbC可能是他想要的稻草。

DbC体现了一种契约精神

为了寻找答案,袁帅继续查阅了一些学习资料,得到了很多有益的信息:

早在1986年,伯特兰·迈耶就提了出Design by Contract[1],这哥们还设计了Eiffel编程语言来实现这种设计思想。2003年,由伯特兰·迈耶创建的Eiffel Software公司申请将Design by Contract作为商标,并于2004年12月获得授权。

看完这段,袁帅感慨由生:”我的天哪,老外的知识产权意识很强烈呀,连这也要申请专利。等我发明出新概念,我也得去搞一个…”

Design by Contract,按契约设计,也叫契约编程,它规定软件设计人员应为软件组件定义正式、精确和可验证的接口规范,该规范应使用前提条件后置条件不变式来扩展抽象数据类型的普通定义。根据对商业合同的条件和义务的概念隐喻,这些规范被称为合同,也就常说的契约。

图片来自:https://en.wikipedia.org/wiki/Design_by_contract

袁帅看到这个官方解释,还没法一下子消化掉。好在DbC应用了他熟悉的霍尔逻辑[2]。在霍尔逻辑中,霍尔三元组对理解程序更友好:

{P} C {Q}

P和Q是断言,C是命令 。P叫做前置条件,Q叫做后置条件。霍尔三元组简单理解为:只要P在C执行前的状态下成立,则在执行之后Q也成立。

有了熟悉的概念加持,袁帅有种雨过天晴、云雾散开的轻松愉悦感。看了一眼桌子上的外卖已经凉了,他简单收拾了一下,拿着水杯起身去厨房品尝刚切好的水果了。

DbC在OOD中的应用

吃了几片鲜橙,袁帅迫不及待回到座位,准备用刚才学到的武器来剖析他的代码。他的专注点又回到了Rectangle和Square。在他的代码中,Rectangle.setWidth(double width)

  • 前置条件是:assert type width is double && above 0.0
  • 后置条件是:assert this.width == new.width && this.height = old.height

但Square.setWidth(double width):

  • 前置条件是:assert type width is double && above 0.0
  • 后置条件是:assert this.width == new.width && this.height = new.width

当然,setHeight的方法也是如此。

从上述的分析来看,派生类Square的前置条件跟基类Rectangle保持一致,后置条件发生了变化,那么对于如下使用场景:

private static void assertStandardHouseArea(Rectangle rectangle) {
    rectangle.setHeight(20);
    rectangle.setWidth(30);
    assert rectangle.calculateArea() == 600;
}
  • 前置条件:assert input is a Rectangle
  • 后置条件:assert rectangle.calculateArea() == 600

假如使用者传入了一个Square实例,因为Square is a Rectangle,前置条件没有变,但后置条件变成assert rectangle.calculateArea() == 900。发生这种现象后,喜欢联想的袁帅,不禁想到一个”遵纪守法”的公民受到了不公的对待一样的生活场景。

但究其原因,归根结底其实因为Square继承了Rectangle之后,违反了Rectangle定下的契约。那么,回到OOD中,按照伯特兰·迈耶的DbC的描述,相比于基类,派生类需要遵守的契约是:

  1. 派生类只能使用相等或更宽松的前置条件。
  2. 派生类只能使用相等或者更严格的后置条件。

到这一步,袁帅心中的疑惑已经差不多解开。他很开心自己找到了根本原因,有了一个清晰的方向,心情舒坦地向靠躺在座椅上,端起了已经快凉掉的枸杞菊花茶。

就在这时,他脑海浮现出一个月前不愉快的买房经历…..

房屋买卖的合同契约

半年前,袁帅迫于结婚的压力,开始四处打听房子。经过5个月的东挑西选,他看中了一家不错的楼盘。销售顾问老吴跟他签了个协议,并加盖了公章,协议里对他的约束是:

  1. 3.15号之前缴纳剩余100万首付款
  2. 支付方式可以是支付宝、储蓄卡或者现金

合同里,开发商需要履行的职责(对开发商的约束):

  1. 3.15号之前房源被锁定,不再对外销售:
  2. 提供24小时热线服务。

后来老吴因为有事情休假了,双方协商后,老吴派他的徒弟小高来服务袁帅。好景不长,2天后,袁帅就接到小高的通知:

  1. 3.13号之前要缴纳剩余100万首付款。
  2. 只能付现金或刷储蓄卡。

袁帅莫名其妙,还没太回过神来,袁帅又从开发商那得知如下实情:

  1. 3.13号小高就将房源对外公布购买。
  2. 小高电话下午8 ~ 10点销售电话一直关机。

袁帅不买账了,作为客户,他自己遵守了跟老吴建立的契约,但是老吴的徒弟小高破坏了这个契约,这个结果他很难接受。后来他也去找跟销售顾问讨说法,好在小高的领导出面协调,事情很快得到解决。

袁帅回过神来,会心一笑,他在纸上写了两点总结了买房子的前后变化:

  • 前置条件更加严格:开发商对我的要求更为苛刻。
  • 后置条件更加宽松:开发商给我承诺和服务打折扣了。

那么按照DbC的思想,老吴的徒弟小高介入后,本应该要做到以下几点:

  1. 派生类只能使用相等或更宽松的前置条件
    • 小高可以让你的支付日期在3.15或者之后
    • 小高可以提供以上三种或者更多的的支付方式
  2. 派生类只能使用相等或者更严格的后置条件
    • 小高可以保证为你锁定房源到3.15号或者更久
    • 小高可以提供24小时热线服务(好像也不能更严格了)

DbC为我们指明了一个方向

喝完首泡枸杞菊花茶,袁帅起身准备去倒第二泡。手机弹出了一个Calendar提醒 – TDD训练营

他决定先不管第二泡了,手指回到键盘,打开了石墨文档,机械键盘铿锵有力的唱起来:

2020年3月15日,我收获颇丰。起初,LSP这个设计原则的学习让我产生了一个疑问 – 如何有效地帮助明确用户合理的假设?经过这个学习过程,我明白客户程序只能基于父类行为方式做出合理的假设,子类不应该去破坏父类定下的契约。Design by Contract设计思想的提出,为我们猜测客户程序的合理假设提供一个依据,也帮助我们设计出遵循LSP的继承体系。

写完日记,发现时间还比较充裕,他果断弄了第二泡,慢悠悠地去往阳光房参加 TDD训练营。路途中,他想起了上周在 重构训练营 中,袁老师提起约定优于配置这个概念时这么说过:

简单来讲,当没有形成一个大家共识且自发遵守的东西(约定)时,我们需要借助附加的流程来进行管控(配置)。渐渐地,当大家心中形成了共识,便不再依赖这些配置。如果你不知道什么样的代码是规范的代码,可以考虑借助一些显性的参考依据,比如代码规范文档。

“伯特兰·迈耶提出的Design by Contract不也是在为我们提供一个这样的参考嘛,它能促使程序设计走向一个更好、更优的状态~ 耶✌️” 袁帅突然察觉到小伙伴们目光都扫向了他,此时的他正斜靠在阳光房门上,面带微笑…

参考阅读

关于更多、更全面的契约式设计介绍,欢迎阅读更多资料:

  1. 契约式设计
  2. 霍尔逻辑
  3. 面向对象软件建构

Posted by 袁慎建 @ March 15th, 2020

版权声明:自由转载•非商用•非衍生•保持署名 | Creative Commons BY-NC-ND 4.0

原文链接:https://yuanshenjian.cn/talk-about-dbc-2/
DbC
⤧  下一篇 《你只是看起来很努力》些许感想 ⤧  上一篇 简单聊聊契约式设计(上)