《基于模型的软件开发》——1.4 技术革新

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

1.4 技术革新

在OO范式之前,即使是编程的“黑暗时代”,也并非一片混沌。学术界一直致力于调整数学运算使其能够应用于计算环境下的实践。经过认真思考,学术界提供了一种数学通用语言作为软件开发的基础。这是一个特别适用于计算环境的理论和模型集合。今天这些数学运算仍然是软件开发时一切工作的基础。
本书的重点为软件开发的工程方法,因此不会涉及很多理论。然而,主要的相关数学元素以及它们对软件开发的影响很值得用几个小节高屋建瓴地描述。因为这些理论现在仍然存在于OO范式背后,尽管是以一种非常隐秘的方式。
1.4.1 图灵机
20世纪30年代阿兰·图灵为理论计算机开发了一个详细的数学运算模型。该模型简明精炼。它假定所有计算过程可以划分为基本的、单个的步骤。这些步骤可以表示为图灵机的一组原子操作指令。一旦这样划分,任何计算过程都可以用以下元素进行描述。
序列:连续存在于程序中的一条或者多条指令的序列。
分支:基于一些布尔条件的真值,将程序的执行从程序某个位置的序列中切换到另一个位置的序列中。
迭代:序列和分支的组合,能够使一个序列无限制地重复,直到某些布尔条件变
为真。
通过严格定义的内容,例如:指令和条件,数学运算深入其中,并且证明具有有限指令集的图灵机确实能够成功地执行任意复杂的计算。为了做到这一点,图灵需要检查基本操作的各种组合、嵌套策略。
多年来有一系列计算模型开发出来,图灵机是其中最有名的。这些模型属于通用类的硬件计算模型。需要特别注意的是,这一类模型描述的是计算机如何工作,而不是软件如何设计。在现实中,几乎整个计算域都是以各种硬件计算模型为基础的。甚至外来的数学结构(例如3GL类型的系统)都基于硬件计算模型。
OO范式代表了脱离硬件计算模型的首次重大突破,它创造了一种软件设计的方法,这种方法至少部分独立于硬件计算模型。并不是说OO范式忽略了这些模型。最终,这些问题解决方案还是要在图灵硬件上执行的,所以OO范式有必要提供一个到硬件计算模型的明确映射。
1.4.2 语言和方法学
之前提到过,编写程序的语言是多种多样的。最初程序编码为二进制指令,直接写入硬件插件板中,直接连接硬件电源导轨组成逻辑1和逻辑0,每次一位(bit)。算术逻辑单元(Arithmetic Logic Unit,ALU)的指令由一个大小固定、直接写入硬件寄存器的二进制字组成。这是非常复杂的,因为硬件通常将这些字细分成称为字段的位子集。
软件开发中的第一个重大进步是BAL(Basic Assembly Language,基本汇编语言)的引入。解决了更大的问题,软件程序的规模也更大了,但是人无法用二进制更好地思考。BAL使硬件操作能够用易于记忆的符号(例如ADD、MOV和JMP等)来进行描述。BAL还使得ALU能够用符号化的R1,R2,…,Rn来表示。最终,BAL使开发人员能够将易于记忆的符号名称分配到内存地址中,使数据能够用一种有意义的方式进行命名。所有这些都是软件开发人员的福音,因为这使得开发人员能够用一种更抽象的方式而不是无穷无尽的0、1字符串来考虑解决方案。
然而,为了使BAL工作,这些符号需要翻译为硬件可以理解的二进制代码。由此引入了程序的一个强大概念,称为编译器,用于处理其他程序,将其从一种表示法转化为另外一种表示法。(原始操作系统已经用于管理通过ALU运行的程序,但是它们并没有大幅度地修改这些程序。)
虽然这一切都为软件开发人员提供了便利,但是仍然存在问题。BAL必须依赖于特定的ALU指令集,而20世纪50年代的硬件制造商生产出了各种各样具有不同指令集的计算机。因此需要一个关于解决方案的、更抽象的、独立于特定指令集的描述。于是20世纪50年代,FORTRAN(FORmula TRANslator)语言和COBOL(COmmon Business-Oriented Language)语言被引入了。就像BAL将软件程序从二进制表示提高了一个抽象层次一样,下一代语言又将抽象层次从ALU提高了一层。
这些3GL为计算引入了一些新的抽象概念。以下是用于刻画这一代语言的主要的抽象概念。
过程:过程是一些指令块,能够在程序的任何地方调用。当指令块执行完成后,控制会返回程序中过程调用的地方。这使得指令集能够在不同的上下文中得到重用,这在大型应用中很常见。
过程消息传递:然而,每一次在不同的上下文中调用过程时,需要修改的数据不同。过程参数能够为特定的上下文提供特定的数据。这些参数数据就成为每个调用上下文发送到过程的一条唯一消息。这朝着过程如何处理数据与数据源上下文之间的去耦合(decoupling)更近了一步。
类型:为了提供这种去耦合,需要一种更抽象的机制来更通用地描述数据是什么,而不涉及源上下文的语义细节。类型使得过程可以更加通用地定义数据(例如:一个整数值)。因此,一个过程能在不知道样本具体值的情况下,计算一组样本的统计值,它只需要知道它们是用于计算均值和T统计量的整数即可。于是,3GL中的类型最初定义数据的实现而非数据的问题语义。
基于堆栈的作用域:由于过程被其他过程所调用,因此操作序列有一个固有的层次结构。过程需要知道如何返回原处,因此诞生了调用堆栈的概念。调用堆栈提供了一种跟踪方法,任务完成之后,嵌套的过程调用可以解开。
过程块结构:过程为功能隔离提供了切实的基础。这种模块化使得分而治之地管理复杂性成为可能。3GL将其扩展为过程内部的模块化。这证明了宝贵的经验,例如,当指令组具有单个入口和出口点时,程序的鲁棒性更好。
因此,20世纪60年代软件开发从BAL发展至3GL是一次意义深远的变革。新概念,例如过程消息传递、基于堆栈的作用域等,需要一个有关构造的全新视角,而不是像BAL对图灵计算所做的那种轻微的抽象。找到突破口之后方法得到了加强。随着3GL被广泛接受,培训开发人员正确使用它们的需求也产生了。到了20世纪70年代早期,毫无疑问,3GL实现的更大程序需要正确地设计。这使得在结构化开发的大范围之中诞生了大量的构造方法,如之前讨论的那样。
这些方法基本上都意识到,解决大的问题不仅仅关乎汇编或者3GL编程技巧。这些抽象构造需要通过某种方式一起正确运行来解决用户的全部问题。一个重要的问题就是认识程序可维护性方面的需要。除此之外,大型程序需要一个结构化的框架,框架一旦确定会变得很难修改,因为有数量众多的独立构造依附于它。于是方法很快发展到比3GL编码更抽象的层次,开发人员在3GL编码中分离出一个独立的设计活动,开始寻找那张“大图”。
编程语言不是可执行文件的唯一表示。有大量图形化表示法也已经用来描述程序。每一种设计表示法,从早期的HIPO图到现在的统一建模语言(Universal Modeling Language,UML),都经常称为图形化表示法,这并非偶然。这些表示法可以有效表达设计所需要的抽象概念。
本书的大多数读者都具有一定的软件开发经验,因此可能对之前讨论的语言和方法相当熟悉。然而,回顾一下是很有必要的,因为本书后面一些非常重要的内容建立在这些基础之上。
以任何一种比二进制机器语言更高级的语言表示的问题解决方案,都是对于在图灵机上实际执行的机器语言的一种更抽象的表示。对于具体事务一种更抽象的表示是模型。因此C语言中的一段程序是问题解决方案的一个模型,最终可以在计算机上执行,UML中OOA模型在计算机上执行时也是如此。
对问题解决方案更加抽象的表示是软件开发持续发展的方向。
因为在计算环境中数学运算具有普适性,所以计算域是确定的。例如:编译器、链接器和加载器为早期的BAL和3GL开发人员自动完成了大量辛苦工作,因为,类似将符号名映射到二进制地址这一类工作是确定性的,即便在复杂的多任务环境中也是如此。
1.4.3 集合和图
对于计算域中各类数学运算普适性的讨论,特别是集合论和图论,超出了本书的范围。不过,其中有几个问题对于OO范式和特定的MBD来说非常重要。
计算域是确定的。因为无论如何变化,某些严格的数学运算都是计算域的基础。这对自动化有深刻的影响。
计算域是有约束的。无论做什么都会受到规则的约束,例如语法、数据结构、标识、GUI设计等。在实现中,一旦特定计算域已经定义,那么问题解决方案实现的*度就十分有限了。软件开发的创造性在于对解决方案的设计,而不是实现。
计算域最终以硬件计算模型为基础。无论我们对解决方案的构造如何抽象,底层的数学运算都能保证它们准确地映射到硬件计算中。
任何软件构造中方法学的第一个步骤都是将非图灵的用户域中没有良好定义的、不精确的、有歧义的概念表达为计算域中完整的、精确的、无歧义的概念。这些表示法使用严格的表达式来表示数学运算,这些数学运算必须与计算域中基本的数学运算完全相同。
结构化开发方法学不仅仅是关于20世纪70年代之前开发实践中宝贵教训的百科全书。相反它们变成了以数学运算作为坚实基础的软件构造的内聚逻辑。无论其表达形式如何不正式,表示法如何简单,每一个基于该数学平台的软件构造方法学都经受住了时间的考验。影响是相互的,计算同时成了开发数学新分支的推动力,由此推动产生的数学分支包括通过类型论表达的关系论和用于非图灵计算的外来的拓扑理论。
1.4.4 范式
与SD同时发展起来的另外一个极为实用的工具是一般集合论的一个特定分支,集合论是数据存储和程序的静态描述的基础。也就是说,它形式化并约束了数据的定义、修改以及与其他数据关联的方式。20世纪60年代E.F.Codd首先对它进行了定义,用于描述关系数据库。
Codd的关系数据模型(Relational Data Modle,RDM)已经同时纳入OO范式和扩展的结构化开发方法之中。例如,面向对象类中的属性(attribute)是Codd 1970年提出的一个术语。一个范围更广的、与之相匹配的数学分支已经开发出来,即关系理论。OO范式就是以这个更宽泛的视角为基础的。
我们在此避免集合论的数学概念,尽管集合论是范式的基础,只提供执行摘要视图,因为开发好的应用所必须了解的只有理论应用的层面。虽然范式被广泛应用,但是表格形式最为易于理解。表本身代表一个n元关系表实体(行),每一个实体共享相同的n属性(列)。也就是说,该表描述一个共享属性特征的实体集。每个属性标识一个特定语义域的可能值。表的每一个实体(行或元组)有一个唯一的标识和一组特定的属性值(表单元格的值)(参见图1-3)。
《基于模型的软件开发》——1.4 技术革新

图1-3 将问题域实体映射到一个n元关系。每个已识别的实体成为关系中的一个元组。每个关系捕获一组唯一的值,用于刻画各个实体的共同特点
在实践层面,一列属性表示一个特定的数据语义,该语义由表中各行共享,它描述这一列中单元格的数据。因此单元格表示这个语义下的特定值,每一行表示一组属性数据,特性值与实体唯一相关。该唯一性与标识有关。每一行必须具有一个区分于其他行的唯一标识,属性值必须依赖于标识。在图1-3中,该标识为人的名字。
在一些应用于数据库的、限制性更强的关系数据模型中,一个或者多个嵌入特性的子集组成该表行中的一个唯一标识符,其他的行不具有全部相同的标识符属性值。标识符属性通常统称为键。不是键的一部分的其他属性在多行中可能具有单独或者组合的重复值,但是给定行中的特定值只取决于该行的键。
关系理论研究的是在不同的n元关系(表)中,某特定实体与其他实体相互关联的方式。为使该理论切实有用,需要制定一些规则,规定在一个n元关系中如何定义属性和实体。规则的集合称为范式,范式对所有现代软件开发都非常重要。
范式的核心要求遵守以下三条规则。
1)属性必须是简单领域的成员。这指的是属性不能进一步细分(即它们的值是标量)。也就是说,如果我们有一个名称为House的表,它的属性为{Builder,Model},我们不能将其中的Model细分为{Style,Price}。问题是对于House表一行中的Model,Style/Price的组合对于单值属性单元格是二义性的。因此House表必须由{Builder,Style,Price}组成。
2)与第一范式兼容的非键属性完全依赖于键。假设House表由以下属性定义{Subdivision,Lot,Builder,Model},其中Subdivision和Lot是键,唯一标识了一所特定的房子。同时假设只有一家建筑商在该地块上进行开发。现在Builder仅仅依赖于Subdivision,同时通过Subdivision间接依赖于Lot。为了能够兼容,我们需要两个n元关系或表:House{Subdivision,Lot,Model}以及Contractor{Subdivision,Builder}。(RDM提供了一种形式用于通过Subdivision属性连接两张表,从而保证我们在确定任何一栋房子的模型和建筑商时都没有二义性。这一概念是后面章节的主题。)
3)与第二范式兼容的非键属性传递依赖于另外的键。基本上,这意味着没有任何一个非键属性值依赖于其他关键字。也就是说,对于House{Subdivision,Lot,Model},如果Subdivision=8,Lot=4,Model=filmsy,那么无论我们在表中添加还是删除行,对于Subdivision=8,Lot=4来说,Model仍然没有任何价值。
通常大多数开发人员使用以下口诀记忆第三范式:属性值依赖于键,整个键,不依赖于其他只依赖于键。
近年来有其他的范式加入,用于处理特定的问题,尤其是在面向对象的数据库领域。我们在此不做讨论,因为它们与我们构建应用和编写代码的方式并不十分相关。然而,上述三大范式的价值非常一般,在定义抽象及其关系的时候非常有趣。
1.4.5 数据流
直到20世纪60年代末,数据一直都是二等公民。最早期的计算机科学理论大多数都产生于大学,在那里计算机系统用于解决科学和工程上的高度算法化的问题。然而,到20世纪60年代末,人们顿悟到世界上大多数软件实际上是用于解决业务问题的,主要用于普通的会计系统中。
特别是,业务问题不是由算法进行刻画的,而是由数据的USER/CRUD处理所刻画的。业务问题与算法科学不同,后者最重要的是为分配做一个长除法。而且,业务问题需要对大量数据进行重复的简单计算。商业界所需要的是处理数据的方式,就像科学界处理操作那样。这导致了各种风格的数据流分析。其基本思想在于:过程是变换数据的小的原子活动,各个过程通过数据流连接,如图1-4所示。
《基于模型的软件开发》——1.4 技术革新

图1-4 计算是否赠予雇员退休礼物的数据流图示例。数据以流的形式提供(例如:雇员ID和年龄)或者从数据存储中获取(例如:出生日期和当前日期)
当我们开发一张数据流图时,它代表宽度优先的控制流。过程是自包含的、原子的,而不是可分解的。所以问题解决方案通过这些过程之间的直接连接进行定义。连接是直接的,仅仅依赖于接下来需要对数据做什么。如果想要解决不同的问题,那么我们只需要改变连接的顺序,使数据以不同的顺序通过相同的原子过程。数据流是进行对等协作(peer-to-peer collaboration)的先驱,对等协作是OO范式的核心,随后会进行详细讨论。
然而,这种看世界的视角与深度优先、以算法为核心的功能分解视角不能很好地结合。试图在一种软件设计范式中统一这些视角的做法导致20世纪70年代开始出现一些很有趣的错误。最终人们放弃了尝试,任由两种方法根本对立。对于易变的、数据丰富的环境,OO范式为主导方法。同时,函数式编程用于进行纯算法的处理,其中不需要持久状态数据,且需求稳定。
在很多方面,纯粹的数据流分析是现代OO范式的先驱。我们之后会看到,小型的、自包含的、内聚的过程已经广泛应用于对象行为职责的思想之中。类似于封装、对等协作等思想驱动了宽度优先的控制流范式。但是在两个方面,它们有很大的不同。OO范式在同一个对象容器中同时封装了行为和数据,而数据流被消息流所取代。在这个主题下需要了解的最重要的一点是,SD基于功能分解,而OO范式与数据流保持更紧密的一
致性。
1.4.6 状态机
同时,软件开发有另外一个分支陷入了比商务人员更大的麻烦中。那些设计实时和/或嵌入式(Real-Time and/or Embedded,R-T/E)硬件控制系统的人正在经历一类与可维护性不同的问题。他们没有办法使自己的东西正常工作。
基本上,他们面临三个问题。第一,这类控制系统经常在资源极度受限的环境中使用。他们没有办法将豪华的IBM 360嵌入到一个洗碗机或者铁路信号中。第二,其他开发人员使用的支持工具无法应用于他们的环境。尤其是当调试程序的时候,他们可能都没有I/O端口用于输出语句,更不用说代码级的调试器。
第三个问题是他们处理的是关于时间的另外一个视角。事件并不同步于事先定义的程序控制流,它们随机出现,与外部硬件同步,外部硬件只要不是计算机就与图灵机格格不入。这些异步处理完全打乱了图灵机中序列、分支和迭代构成的有序世界。随着时间推移,工具变得越来越好,但是R-T/E人员仍然在软件行业中从事着最难的工作。
有限状态机是使得R-T/E人员能够忍受艰辛生活的工具。是谁第一个使用有限状态机写程序已经无从考证了,但是无论是谁,他都有资格与Ada Lovelace等人一起进入软件名人堂。这使得异步处理在串行的图灵世界中成为可能,也使非常简洁的程序成为可能,因为大多数控制流逻辑隐含在有限状态机处理事件的规则之中,以及由状态转换表示的静态顺序约束中,如图1-5所示。
《基于模型的软件开发》——1.4 技术革新

图1-5 简单交通灯的有限状态机示例。交通灯按某种方式在状态之间迁移,这种方式受限于允许的转换。状态转换由外部事件触发
然而,有限状态机真正有趣的地方在于它恰巧解决了OOA/D关注的一些问题。尤其是,它出色地降低了耦合,实现了真正的封装。它还实现了OO设计中一些很好的基础实践。如果使用一个简单的构造来捕获和执行OO范式中的核心元素,那么有限状态机就是这个构造。我们将原因解释放在第三部分。

上一篇:345. 反转字符串中的元音字母


下一篇:pytest之allure(六)之集成缺陷管理系统和测试管理系统(allure.link、allure.issue、allure.testcase)