《基于模型的软件开发》——1.2 结构化开发

简介:

本节书摘来自华章计算机《基于模型的软件开发》一书中的第1章,第1.2节,作者:[美]H. S.莱曼(H. S. Lahman)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

1.2 结构化开发

结构化开发无疑是20世纪80年代之前最重要的单个技术进步之一。它第一次为软件开发提供了真正意义上的系统化方法。与20世纪60年代的3GL结合,结构化开发能够使生产力取得重大进步。
结构化开发具有一种很有趣的边界效益,这在当时未被注意到。应用更加可靠,当时人们没有留意这一点,因为软件应用得越来越广泛,对非软件的用户的可见性越来越高;软件依然存在很多缺陷,用户仍然认为软件是不可靠的。事实上,可靠性从20世纪60年代初的150个缺陷/千行降低到了20世纪80年代的15个缺陷/千行。
结构化开发实际上是一个广泛的概念,覆盖了范围很广的各类软件构造方法。这些方法通常都有如下特征:
图形化表示:这些新兴的方法都有自己的一些图形表示形式。其中潜在的简单原则就是“一张图胜过千言万语”。
功能隔离:基本原理是程序由复杂性不同的大量算法构成,它们共同工作以解决一个给定问题。因为程序开始变得庞大而无法由一个人在合理的时间内完成,所以产生了交互算法这个相当有开创性的概念。功能隔离将这一算法在形式上转化为可重用的函数库、子系统以及应用层次等。
应用编程接口(Application Programming Interfaces,API):当进行功能隔离的时候,仍然需要用某种方法访问。这就催生了不变量接口的概念——所有客户端使用相同的方式访问,并且当功能进行修改时,用户可以不必了解变更的细节。
契约式编程:这是API的一个逻辑延伸。API本身就是服务及其客户端之间的一个契约。契约真实地描述了关于服务的语义,而API仅仅定义了可以获得该语义的语法。当语言开始将类似于行为的断言的内容作为程序单元的一部分时,这个概念才开始变成一个正式的契约。这是一个好主意的好开始。
自顶向下开发:该方法最早的构想是,从高层次开始,抽象用户需求,逐步将其细化为更加特定的需求,这些特定需求与计算环境有关,而且更加详细。自顶向下开发与功能分解恰巧能够完美匹配,功能分解的概念随后解释。
分析与设计的出现:在SD定义下,开发活动不仅仅是写3GL代码。分析是需求获取、需求分析、用户域规格说明和开发人员领域中高层次软件设计活动的一种综合。设计是一个正式的步骤,在敲键盘编写3GL代码之前,开发人员为软件结构的细节提供一个图形化的描述。
结构化开发使程序的构造较之以前有更好的可维护性。实际上,在专家和训练有素的开发人员手中,这些方法能够使程序像现代OO方法开发的程序一样具有可维护性。问题在于,达到这一点所需要的专业能力让大多数软件开发人员望尘莫及。所以另一颗银弹失去了得分机会,但至少这种方法在纸上得到了实现。然而,值得注意的是,以上的每一种特征都可以在现代的OO开发中找到(其中一些特征,例如自顶向下的设计,所起的作用非常有限)。
功能分解
这是所有结构化开发方法的核心设计技术,结构化开发方法中的结构化指的就是这个。功能分解将解决方案作为算法进行处理。这是一个更加科学的视角,而非信息系统编程的管理学视角。(3GL的第一种语言FORTRAN是公式翻译器FORmula TRANslator的缩写。)结构化开发也更接近于硬件计算模型,该模型在之后讨论。
功能分解的基本原则是分而治之,通常通过一种典型的自顶向下的方式,将大的、复杂的功能分解为小的、更易于管理的组件算法。这就形成了一个倒置的树结构,顶上的高层次功能调用一组包含细分功能的低层次功能即可。树的叶子节点是原子功能(按照算术运算符的规模),原子功能不能进一步细分。图1-1是一个功能分解的例子。

image

功能分解强大而具有吸引力。强大是因为它非常适合在算法计算机器图灵机的世界中管理复杂性,尤其是将第三代计算机语言提供的程序作为基本的语言构造时。在充满了复杂算法的世界中,功能分解是非常直观的,因此有了功能分解的科学界就如同喝了烈酒获得了“手枪之夜”入场券的卡车司机一般。
功能分解具有吸引力是因为它囊括了以下概念:功能隔离(例如:树的叶子节点以及细分功能的详细描述);在细分的功能中应用契约编程为高层客户提供服务;一旦定义完分解树就可以形成自顶向下的开发技术路线图;过程签名形式的各种API;控制流采用最基本的深度优先遍历;以及在不同的程序上下文中通过调用同一个叶子节点实现重用。总之,在处理很多离散问题时,这是一种非常明智的做法。
然而,在20世纪70年代末结构化开发方法明显遭遇了一些之前没有人预料到的新问题。这些问题与以下两个正交的现实有关:
在科学领域算法不会变更,通常它们或者经得起时间检验,或者被新的、更高级的算法完全替代。然而,在商业程序等领域中,规则不断变化,产品持续进化。因此,应用在其使用的整个生命周期中都需要修改,有时应用甚至在开发的早期就要修改。
层次结构是很难修改的。
问题在于,功能分解本质上是一种深度优先的范式,因为只有当所包括的子节点功能全部实现之后父节点的功能才能实现。当需求变更的时候,这种控制流严格的自顶向下层次结构很难修改。变更控制流往往意味着对子节点的完全重组。
另一个问题是冗余。原子功能通常具有相当的局限性,因此不同的子节点通常需要使用其他子节点所需的很多相同的原子操作。不同的子节点往往以相同的顺序重复相同的原子叶子操作序列。构造这些冗余子节点是乏味的,而维护时,这又会成为一个严重的缺陷。多个子节点中相同的变更不得不多次重复。这样的重复增加了引入出错的概率。在20世纪70年代末,冗余代码被广泛认为是在维护中造成低可靠性的重要原因之一。
为了根治这一问题,更高层次的服务定义出来以一次性捕获这些操作的特定序列,然后通过在功能树的不同子节点中调用它们实现重用。这解决了冗余的问题,但是引发了一个更糟糕的问题。在一个纯粹的功能分解树中,每一个过程有一个确切的客户,因此树具有高度的针对性。跨子节点重用的难点在于,树结构变成了一个网格结构,服务同时具有很多下层子功能节点和调用它们的父客户,如图1-2所示。
图1-1中的应用扩展后用于计算多名员工的收益,这些计算都需要完成基本相同的任务。有一些任务需要以特定的方式实现,从而使基本树上计算每一种收益的节点具有相同的结构。但是有一些任务基本相同。为了避免冗余,树上那些计算不同收益的节点以客户端的形式重用上述以特定方式实现的节点。由此产生了类网格结构,结构中有一些任务(例如“在数据库中获得基本工资”)具有多个客户端,这些客户端存在于应用完全不同的多个部分之中。
网格结构在可维护性方面存在很严重的问题。当一个特定节点中的一些高层次功能(例如:图1-2中的保险收益)发生需求变更的时候,变更有可能通过树上较低层次的子功能实现。然而,因为重用的关系这个子功能可能有来自于不同节点的多个客户端(例如:股票收益)。这些需求的变更有可能不应用于其他的子节点客户端(例如:股票收益),这些子节点客户端在不同的上下文中重用了该项子功能。因此,为了一个客户端在低层做出变更,将会在其他的上下文中影响其他客户端。
image

图1-2 功能分解树变为网格。带阴影的任务是针对不同收益的基本元素。虚线表示从一个分解子节点到另一个子节点的交叉,用以消除冗余
然而,功能分解的最大困难来源于深度优先过程自带的上下文知识中隐含的知识。基本范式就是做这些。高层次的功能基本上是指示低层次功能实现的命令集合。为了发出命令,高层次功能需要知道低层次功能:1)由谁实现;2)何时可以实现;3)在整个解决方案中的次序。所有这些都在一定程度上打破了功能隔离,最后一点最容易暗藏危险,因为它要求调用功能模块了解整个问题解决方案中较高层次的上下文。高层次功能将知识固化于实现中,所以当整个上下文的需求变更时,实现就必须变更。这种层次功能的依赖,加上刚才提到的其他问题,会导致传说中的“复杂管线”(spaghetti)
代码。
规格说明和DbC契约是看待这一问题的另外一个角度。当一个功能被调用的时候,它与调用它的功能客户端之间存在“做这些”的契约。这一DbC契约表示了对功能所提供服务的一种期望。该功能是自己提供整个服务(例如,不进行细分),还是将部分或全部功能交给更低层次功能来实现并不重要。从客户端的角度,与当前功能的契约是针对整个服务的。如果当前功能在树上处于较高的层次,那么这个功能的规格说明就是它所包含的所有子节点的规格说明。从高层次功能分解而来的较低层次的功能是高层次功能的一个扩展,它们各自的规格说明是高层次功能规格说明的子集。
这就意味着,在网格中分解自一个给定的高层次功能的那些子节点与高层次功能之间形成了一种复杂的依赖关系。也就是说,为了履行与其客户端达成的DbC契约,高层次功能依赖于每一个分解自它的低层次功能是否正确完成了客户端对其所期望的事情。这样的依赖链是产生“复杂管线”代码的根本原因。在没有确定对潜在的父节点(客户端)规格说明的影响之前,较低层次功能的规格说明是不能变更的。
操作序列中也存在类似的依赖问题。修改叶子节点的调用序列时,高层次功能的实现可能会受到影响。也就是说,序列是以一种系统化的、深度优先的方式向上和向下移动子节点决定的。树的导航在高层次功能的实现中进行了硬编码。因此要变更序列,就需要变更多个高层次功能的实现。实际上,控制流的整个解决方案硬编码到了树结构本身,这有时甚至导致需要重组整棵树来满足一个小的顺序变更需求。
对于同一个问题,另外一种明显的表现出现在对高层次功能做单元测试的时候。由于高层次功能的规格说明包括了每一个向下分解的低层次功能的规格说明,因此在任一向下分解的低层次功能没有实现的情况下,从客户端角度高层次功能的单元测试是不可能进
行的。
一个有力的证据是:OO范式的主要目标在于彻底消除功能分解产生的层次依赖。
没有人预料到这些问题,这并不奇怪。这些问题只有实现了结构化开发设计的生产力之后才会出现。随着应用越来越大,需求变更越来越多,功能分解树的规模和复杂性也越来越高。只有在利益实现时这些问题才会显现。

相关文章
|
25天前
|
机器学习/深度学习 数据采集 监控
大模型开发:描述一个典型的机器学习项目流程。
机器学习项目涉及问题定义、数据收集、预处理、特征工程、模型选择、训练、评估、优化、部署和监控。每个阶段都是确保模型有效可靠的关键,需要细致操作。
17 0
|
1月前
|
机器学习/深度学习 数据采集 传感器
机器学习开发流程和用到的数据介绍
机器学习开发流程和用到的数据介绍
|
数据建模 Linux 数据库
简单实用的数据建模工具PDManer
PDManer是一款开源的国产数据建模工具
9922 0
简单实用的数据建模工具PDManer
|
9月前
|
缓存 自然语言处理 前端开发
【自然语言编程实践】GPT4 企业级在线商城开发 01-数据模型设计 下
【自然语言编程实践】GPT4 企业级在线商城开发 01-数据模型设计
|
9月前
|
敏捷开发 自然语言处理 前端开发
【自然语言编程实践】GPT4 企业级在线商城开发 01-数据模型设计 上
【自然语言编程实践】GPT4 企业级在线商城开发 01-数据模型设计
【结构化开发方法】系统分析
【结构化开发方法】系统分析
121 0
【结构化开发方法】系统分析