按合约设计

最近看了很多设计文档,产生了一个疑惑:很多参数和变量都是协商式的,这样不会给后面的产品迭代留下很多坑吗?带着这个问题,最终还是在《程序员修炼之道》中找到了答案:按合约设计

经常由于业务上面的需要,我们可能在一个公共平台上面做我们自己的事情,每种业务的前提和产出可能并不近相同,比如说同是使用了RCM内核的RSAS和WVSS设备的产出都不尽相同:RSAS的任务状态输出是阻塞的,而WVSS设备的状态输出是持续的,软件设计中为解决这类软件沟通问题,有一个法则就是按合约设计。

合约既规定你的权利与责任,也规定对方的权利与责任。此外,还有关于任何一方没有遵守合约的后果的约定。

或许你有一份雇佣合约,规定了你的工作时数和你必须遵循的行为准则。作为回报,公司付给你薪水和其他津贴。双方都履行其义务,每个人都从中受益。

DBC

Bertrand Meyer为Eiffel语言发展了按合约设计的概念。这是一种简单而强大的技术,它关注的是用文档记载(并约定)软件模块的权利与责任,以确保程序正确性。什么是正确的程序?不多不少,做它声明要做的事情和程序。用文档记载这样的声明,并进行校验,是按合约设计(简称DBC)的核心所在。

软件系统中的每一个函数和方法都会做某件事情。在开始做某事之前,例程对世界的状态可能有某种期望,并且可能有能力陈述系统结束时的状态,Meyer这样描述这些期望和陈述:

    • 前条件。为了调用例程,必须为真的条件;例程的需求。在其前条件被违反时,例程决不应被调用。传递好数据是调用者的责任。
  • 后条件。例程保证会做的事情,例程完成时的状态,例程有后条件这一事实意味着它会结束:不允许有无限循环。
  • 类不变项。类确保从调用者的视角来看,该条件总是为真。在例程的内部处理过程中,不变项不一定会保持,但例程退出、控制返回到调用者时,不变项必须为真。

按合约设计的实例一

这里先以一个简单的数值插入到一个唯一,有序的列表中为例子,如图 1:

列表插入数值

列表插入数值

通过这个简单的例子我们主要认识一下什么是DBC的概念:

  • 前条件:确保列表中没有这个节点,这是因为这个列表是一个唯一有序表,该条件应该由调用者检查。
  • 后条件:插入新节点后,该节点包含在列表中,当然顺序也是需要确保的。
  • 类不变项:指的是表的唯一有序性不变。

后条件常常要使用传入的方法的参数来校验正确的行为。但如果允许例程改变传入的参数,你就有可能规避合约。Eiffel不允许有这样的事情发生,但java却允许这样。这里我们使用Java的关键字final指示我们的意图:参数的方法内不应被改变。

按合约设计实例二

我们以SaaS部门的fileserver模块为例,理解按合约设计的理念,fileserver设计框架,如图 2:

fileserver业务流程图

fileserver业务流程图

Fileserver的设计较为简单,核心是各个上传、下载、搜索的插件,视图部分主要实在确保插件的前条件,图 2是scancenter与fileserver的业务流程图,它只是fileserver中的一个插件流程。

  • 确保前条件。Fileserver在调用scancenter的上传插件前,首先要确保上传的日志文件符合固定的文件名格式,status的cvs日志文件和report的xml文件,只有文件属于这两种类型,scancenter的插件才能够运行,如图 3。
  • 确保后条件。Fileserser与scancenter约定,插件的解析结果的格式和所放置的redis队列,当有日志上传上来之后,fileserver的插件应该需要将status日志解析并放入‘nsfocus:scancenter:task:status’队列,而把report日志放在‘nsfocus:scancenter:task:result’队列,对队列中的格式也有要求,信息不能被改变。
  • 不变项。Fileserver不变项的设计体现并不明显,它并没有自身的状态可能会被转移或者改变,但是针对不同scan_type机器上传上来的日志,放入队列中的格式总是统一不变的,队列也是不变的。
 scancenter上传前条件

scancenter上传前条件

在上面的例子中,如果任何一方没有履行合约的条款,(先前约定的)某种补偿措施就会启用——例如,引发异常或者终止程序(这里fileserver,将不调用此插件,并不做日志记录)。不管发生什么,不要误以为没能履行合约是bug。他不是某种绝不应该发生的事情,这也就是什么前条件不应被用于完成像用户输入验证这样的任务的原因。

所有的合约都是在文档中进行记录的,如果对于设计有任何疑问,应该优先查看合约设计的文档,也就是开发设计文档,根据文档中的规定来检验合约的正确性。

谁负责?

谁负责检查前条件,是调用者,还是被调用的例程?如果作为语言的一部分实现,答案是两者都不是:前条件是在调用者调用例程之后,但是在进入例程自身之前,在幕后测试的。因而如果要对参数进行任何显式的检查,就必须由调用者来完成,因为例程自身永远也不会看到违反了前条件的参数。

考虑一个程序,它从控制台读取数字,通过调用sqrt来计算平方根,并打印结果。Sqrt函数有一个前条件——其参数不能为负。如果用户在控制台上输入负数,要由调用代码确保它不会被传给sqrt。该调用代码有许多选择:它可以终止,可以发出警告并读取另外的参数,也可以把这个数变成正数,并在sqrt返回的结果后面附加一个“i”。无论其选择是什么,这都肯定不是sqrt的问题。

通过sqrt例程的前条件中表示平方根函数的参数域,你把保证正确性的负担转交给了调用者——本应如此。随后你可以在知道了其输入会落在有效范围内的前提下,安全地设计sqrt例程。

设计文档

回答最开始的疑问,之所以按合约设计是因为业务的需要,fileserver最初的设计是要用来传递设备日志的,后来在它之上做了scancenter应用,fileserver中的插件与scancenter应用的协商属于设计的一部分,这部分约定可能并不适用于其他的插件,但是确实可以满足scancenter的需要,我们强调软件设计的重用,推荐使用通用的插件,但是有时候仅仅是因为业务的需要,我们需要使用合约的方式协商软件沟通,这个时候就需要有充足的文档来记录合约,以便在后来的版本更新,模块维护交接过程中有章可循,有文可参考。

参考:

[1]   马维达. 程序员修炼之道[M]. 电子工业出版社, 2015-07.

如果您需要了解更多内容,可以
加入QQ群:486207500、570982169
直接询问:010-68438880-8669

发表评论