COBOL考古(九)
程序结构
在本章中,我们将讨论结构化编程的概念以及它与COBOL的关系。我们将重点介绍COBOL语言中的关键技术,使您能够编写良好结构化的程序。
编程风格
什么是结构化编程
什么是面向对象编程
COBOL编程风格
Procedure Division的结构
程序的控制和流程
内联和外部执行语句
使用PERFORM编写循环
使用GO TO关键字学习不良行为
段落作为代码块
设计段落的内容
段落的顺序和命名
段落控制程序
PERFORM TIMES
PERFORM THROUGH
PERFORM UNTIL
PERFORM VARYING
使用子程序
指定目标程序
指定程序变量
指定返回值
使用复制文件(Copybooks)
总结
实验
编程风格
在我们更详细地讨论如何构建COBOL程序的结构之前,了解COBOL是一种什么类型的语言以及它与其他语言有何不同,以及它如何影响您可能构建程序的方式是非常重要的。
什么是结构化编程
结构化编程是一组编程风格的名称,包括功能性编程、过程性编程等。结构化编程技术使程序逻辑更容易理解和维护。结构化编程语言的示例包括C、PL/I、Python以及当然COBOL。这些语言提供了特定的控制流结构,如循环、函数和方法,允许程序员以有意义的方式组织他们的代码。
非结构化编程构造,也称为意大利面代码,是诸如GOTO或JUMP之类的概念,它们允许执行流在源代码中疯狂地分支。这样的代码很难分析和阅读。虽然COBOL确实包含这些结构,但重要的是要谨慎使用它们,不要将它们用作良好结构化代码的主要组成部分。
良好结构化的代码既易于理解又易于维护。很可能在您的职业生涯中的某个时候,您将需要阅读并从别人的代码中工作,通常是在它最初编写后的十年后。如果原始作者良好地构建了他们的代码,并且如果有人阅读您的代码,同样也是如此,这对您将非常有帮助。
什么是面向对象编程
面向对象编程,或OO编程,不同于结构化编程,尽管它借用了许多相同的概念。在面向对象编程中,代码分割成多个类,每个类表示系统中的一个角色。每个类由变量和一系列方法组成。类的实例化或对象可以执行另一个对象的方法。OO程序中的每个类仍然可以被视为结构化程序,因为它仍然包含方法和迭代结构。但是,正是通过从一组独立类中组成程序的方式使OO编程与众不同。虽然可以编写面向对象的COBOL,但并不受一些提供COBOL API的中间件产品的支持。它通常不在市场上使用,因此在本课程中不涉及它。
COBOL编程风格
COBOL并没有像C或Java这样的结构化编程语言中常见的一些组件。COBOL不包含for或while循环,也不包含定义的函数或方法。因为COBOL旨在成为一种易于阅读的语言,这些概念通过使用PERFORM关键字和段落的概念来体现。这使程序员仍然可以创建这些结构,但以一种易于阅读和遵循的方式。
Procedure Division的结构
正如您已经了解的那样,COBOL程序分为几个部分,包括标识、环境和数据。然而,本章关注的是如何构造程序部分的内容,以便在将来易于阅读、理解和维护。
程序控制和基本程序的流程
通常,COBOL程序的执行从过程部分中的第一条语句开始,并顺序执行每一行,直到达到源代码的末尾。例如,看一下示例1。这是来自TOTEN1的代码片段。这是一个简单的程序,显示一个简单的消息,计数到十。
1 |
|
Example 1. Snippet from TOTEN1
尽管这段代码很容易阅读,但它并不太优雅,因为数字逐渐增加时存在大量的代码重复。显然,我们希望为程序提供一些结构。有三个关键字可以用来将控制转移到源代码的不同部分并提供所需的结构。这些关键字是PERFORM、GO TO和CALL。
内联和外部PERFORM语句
PERFORM关键字是COBOL语言中非常灵活的元素,因为它允许进入函数和循环。在最基本的级别上,PERFORM允许将控制转移到代码的另一个部分。一旦执行了这个部分,控制就会返回到下一行代码。看下面的例子:
1 |
|
在这个示例中,构建新的输出行并打印它的三行代码已提取到一个名为WRITE-NEW-RECORD的新段落中。然后,通过使用PERFORM关键字,对该段落执行了十次。每次使用PERFORM关键字时,执行都会跳转到WRITE-NEW-RECORD段落,执行其中包含的三行代码,然后返回到PERFORM语句后面的行。段落的概念将在本章后面更详细地介绍。
使用PERFORM来编写循环
到目前为止,我们构建的代码仍然不是最佳的,PERFORM语句十次的重复不够优雅,可以进行优化。观察以下代码片段:
1 |
|
示例3. 来自TOTEN2的代码片段
在这个示例中,我们使用PERFORM关键字的方式类似于其他语言中的for循环。循环从PERFORM关键字到END-PERFORM关键字运行。每次执行迭代循环时,COUNTER的值都会递增,并逐个测试。为了进行比较,在其他语言中,同样的循环将被写成如下:
1 |
|
示例4. Java示例
尽管COBOL版本可能比其他语言中的for循环更冗长,但更容易阅读,而且请记住,如果您使用的是一个好的编辑器,您总是可以使用自动完成来帮助您输入。
使用GO TO关键字学习不良习惯
程序员往往对编辑器的选择、制表符或空格等问题有强烈的信仰,已经就这类问题进行了许多激烈的讨论。然而,如果有一件事我们可以达成一致,那就是使用GO TO通常是一个坏主意。为了演示为什么GO TO可能是一个糟糕的主意,我们将再次查看TOTEN2,并将第二个PERFORM关键字的实例替换为GO TO,如示例5所示。
1 |
|
示例5. 使用GO TO的示例
如果我们编译并运行程序,您会看到尽管作业以4038-abend代码异常结束,但它确实执行了一些代码并写入了输出的前两行。如果您仔细查看输出,会看到类似以下的消息:
1 |
|
示例6. 来自GO TO示例的异常
那么,当我们使用GO TO命令时,到底发生了什么糟糕的事情呢?要回答这个问题,我们需要理解GO TO和PERFORM之间的关键区别。在第一行中,我们使用了PERFORM关键字,将控制转移到WRITE-NEW-RECORD段落。一旦执行到达该段落的末尾,执行就会返回到PERFORM语句后面的行。下一行使用了GOTO关键字,再次将控制权转移到WRITE-NEW-RECORD段落,该段落打印了第二行输出。然而,当该段落完成后,执行继续执行WRITE-NEW-RECORD段落后面的下一行代码。由于WRITE-NEW-RECORD段落后面没有代码行,处理器试图执行程序之外的代码,z/OS将其视为问题并异常结束了程序。
正如我们所看到的,使用GO TO会导致执行分支,不会返回到发出它的代码行。让我们演示一下,这段代码有多混乱:
1 |
|
示例7. 使用GO TO的混乱代码
这个示例使用了混合条件和非条件的GO TO语句,并包含了行号以便更容易跟踪代码。第2行执行,如果标志变量设置为1,将分支到第9行的SAY-HELLO-WORLD。在这种情况下,它是,所以我们通过第9-12行继续,然后分支到第4-6行,其中更新了标志的值并再次测试以查看是否应该跳转到SAY-HELLO-COBOL。由于标志的值不再为1,执行会继续到第7行,然后跳转到第19行并完成运行。将此程序的第5行注释掉,然后再次运行程序。跟踪程序的执行。乱糟糟的对吧?
注意:条件GO TO语句的TO和ON部分都可以省略,得到的语句看起来像GO SAY-HELLO-WORLD DEPENDING FLAG。虽然不够冗长,但同样易于理解。
那么,为什么要教你一些我们说过混乱且不建议使用的东西呢?嗯,通过让您了解其行为,您将更好地了解查看现有代码并维护代码时的工具。
段落作为代码块
到目前为止,在本节中,我们已经使用了一些段落的示例,但并没有真正解释它们是什么,它们是如何工作的,以及它们可以用来做什么。本节将解决这个问题。
在COBOL中,最类似的思考段落的方式是将其视为另一种语言中不接受参数、不返回响应并更改全局变量的函数或方法。它基本上是一个代码块,执行一系列操作,可以在同一个程序中多次使用。
段落在过程部分内定义,从第八列开始,并且可以具有用户喜欢的任何名称,除了COBOL关键字之外,段落的声明由句点(.)完成。段落可以包含一个到多个COBOL句子,并且可以由另一个段落的开始或程序的物理结束来终止。
注意:段落也可以由END-PROGRAM、END-METHOD、END FACTORY或END-OBJECT结束。其中大多数用于面向对象的COBOL,这里不讨论。
考虑到一个程序可以由多个段落组成,并且可以使用PERFORM关键字来调用段落,无论是有条件地还是作为循环的一部分,很容易看出,良好的段落设计确实有助于使您的COBOL更具结构性和可读性。
设计段落内容
段落内的内容没有限制,但是可能希望将代码重构到段落中的原因有两个主要原因:
将一系列COBOL句子组合在一起,以实现特定的功能或任务,例如,打开应用程序正在使用的所有文件、计算特定功能或执行一些数据验证。将这些句子分组到一个段落中允许您为它们提供一个解释代码行用途的名称。
这些句子序列将在循环中使用。将这些行提取到段落中,然后使用PERFORM关键字创建循环可以使代码非常易于理解。
记住,您还可以在现有段落中执行其他段落。这种嵌套调用段落的方式可以帮助结构化代码。
段落的顺序和命名
段落在COBOL程序中出现的顺序没有要求。可以在声明段落之前或之后从某一点调用段落。尽管语言没有强制执行的限制,但有一些技术可以遵循,使更大的程序更易于理解和遵循。一些这些技术和最佳实践包括:
使用与其功能或行为相对应的名称命名每个段落。一个命名为OPEN-INPUT-FILES的段落比一个命名为DO-FILE-STUFF的段落更容易理解。
按照运行时将执行的一般顺序对段落进行排序。这样做有两个主要优点。在现代IDE中使用大纲视图将允许您从上到下“阅读”每个段落的名称,通过这样做,您将能够确定程序的一般结构和行为。
一些COBOL程序员在段落名称前面加上一个数字,该数字会随着源代码的增加而递增,如示例8所示。
由于段落是按照数字编号并按照这个顺序出现在源代码中的,所以当一个句子引用一个段落时,更容易知道程序中该段落可能出现在何处。在最初以这种方式构建程序时,使用的数字仅会递增最高有效数字,以允许根据需要在其中插入新段落。尽管现代编辑器的兴起允许制作大纲并立即跳转到引用或声明,但了解这种技术仍然很有用。
1 |
|
示例8. 编号段落
- 最后,通常通过在每个段落后编写一个空段落来显式结束段落,参见示例9。这个空段落不包含任何代码,与它所关闭的段落具有相同的名称,后缀为-END,并在接下来的段落开始时被关闭。但它可以用作视觉分隔符,在使用PERFORM THRU关键字时很有用,这将在本章进一步讨论。一些学过COBOL的Java程序员评论说,这相当于代码块末尾的闭合大括号(“}”)。
1 |
|
示例9. 显式关闭的段落
使用段落进行程序控制
在本章中,我们已经讨论了使用段落来构建代码结构的重要性。在这个过程中,我们已经多次使用了PERFORM关键字来执行我们创建的段落。具体来说,我们单独使用了这个关键字,并且还使用了它与VARYING关键字一起构建循环。在本节中,我们将更详细地讨论如何使用PERFORM关键字。
PERFORM TIMES
重复执行一个PERFORM语句的最简单方法也许是使用TIMES关键字,以静态次数执行一个段落或一段代码,示例见示例10。
1 |
|
示例10. TIMES
代码应该执行的次数可以是一个文字值,就像上面的示例一样,也可以是一个数值变量的值,如示例11所示,其中PERFORM关键字用于执行一个段落。
1 |
|
示例11. TIMES 2
PERFORM THROUGH
有时候,您可能需要按顺序执行一系列段落,而不是单独执行它们。可以使用THROUGH或THRU关键字来列出列表的起始段落和结束段落。执行将按照它们在源代码中出现的顺序依次进行,从开头到结尾,然后返回到初始perform语句后面的行,示例12为例。
1 |
|
1 |
|
示例12. 使用PERFORM THRU
注意: THRU关键字的使用也可以与TIMES、UNTIL和VARYING关键字一起使用,以允许执行段落的列表,而不仅仅是单个段落或代码块。
PERFORM UNTIL
在PERFORM语句中添加UNTIL关键字允许您在满足布尔条件之前迭代一组语句。实际上,这使您能够在COBOL中编写while循环,看看这个基本示例:
1 |
|
示例13. 使用PERFORM UNTIL
这与以下Java代码等效:
1 |
|
示例14. Java while循环
在这种情况下,布尔条件在执行循环之前进行评估。但是,如果您希望在评估条件之前至少执行一次循环,可以修改语句如下所示:
1 |
|
示例15. 使用PERFORM WITH TEST AFTER UNTIL
这类似于Java中的 “do while” 循环:
1 |
|
示例16. Java while循环
PERFORM VARYING
我们已经在标题为”使用PERFORM编写循环”的部分中使用了VARYING关键字,回忆一下:
1 |
|
示例17. 基本循环
在这个示例中,变量counter被测试,以查看它是否等于11,只要它不等于11,它就会被递增,然后执行在perform语句中嵌套的语句。这个结构可以扩展,如示例18所示。然而,在这个示例中,我们只能执行段落,而不能执行嵌套的语句。
1 |
|
示例18. 扩展循环
这可能看起来复杂,但与这个Java伪代码相比较:
1 |
|
示例19. Java扩展循环
这实际上只是两个嵌套在一起的for循环。当在外部变化循环的每个循环中,内部循环将执行五次。正如之前提到的,条件的测试将被COBOL假定为在循环的开头进行,但可以通过在初始perform语句中添加短语WITH TEST AFTER来指定在循环的末尾进行评估。
使用子程序
到目前为止,我们只研究了单个COBOL程序的内部结构。随着程序在功能和数量上的增加,程序员通常希望将程序的某些功能提供给系统中的其他程序。将通用功能抽象为它们自己的程序,并允许它们从其他程序中调用,可以减少系统中的代码重复量,从而降低维护成本,因为只需要对共享模块进行一次修复。
注意:尽管在这里我们将描述调用另一个程序的COBOL本地方式,但请注意,某些中间件产品可能提供了增强的API来执行此操作。
在调用另一个程序时,我们需要考虑三个主要问题:我们将如何引用要调用的程序,我们想要发送到目标程序的参数,以及我们希望目标程序返回的参数。
指定目标程序
为了调用目标程序,我们将使用CALL关键字,后跟对我们希望调用的目标程序的引用。做这个的两种主要方式是通过文字值或通过引用变量,如示例20所示。
1 |
|
示例20. 基本CALL
还可以通过传递指向目标程序的指针引用来引用目标平台。如果您认为将指针引用传递给函数只是一些超现代的语言才有的功能,那么错了,COBOL早就实现了这个功能!
指定程序变量
现在我们已经确定了要调用的程序的名称;我们必须确定调用程序可能想要发送的变量。这些变量由USING关键字逐个指定。COBOL支持传递引用和传递副本,以及传递值的概念。支持的每种传递技术都可以应用于传递或选择性地应用于不同的项目。
默认情况下,COBOL将通过引用传递数据项。这意味着调用程序和目标程序都可以读取和写入由变量表示的内存区域。这意味着如果目标程序更新了变量的内容,那么一旦执行返回,这些更改将对调用程序可见。
使用BY CONTENT短语允许将传递的变量的副本传递给目标程序。虽然目标程序可以更新变量,但这些更新对调用程序不可见。
注意:在传递变量时,无论是通过引用还是通过内容,请注意可以发送任何级别的数据项。这意味着您可以传递整个数据结构,对于处理常见记录非常方便。
您还可能看到在CALL语句中使用了BY VALUE短语。BY VALUE与BY CONTENT类似,它传递了变量的内容副本。不同之处在于,只支持COBOL数据类型的子集,并且只能指定基本数据项。这是因为BY VALUE主要用于当COBOL调用另一种语言(例如C)的程序时。
指定返回值
最后,RETURNING短语用于指定应该用于存储返回值的变量。这可以是数据部分中声明的任何基本数据项。请注意,这是可选的。某些程序可能不返回任何内容,或者您可能已经通过引用将值传递给目标程序,在这种情况下,目标程序返回后将会看到这些变量的更新。
使用复制本
如果您的程序包含频繁使用的代码序列,我们可以将代码序列编写一次并将它们放入COBOL复制库中。这些代码序列称为复制本。然后,我们可以使用COPY语句在编译时检索这些代码序列并将它们包含在其中。以这种方式使用复制本将消除重复的编码。
我们需要在我们使用的JCL中指定一个复制库。如果您使用提供的过程(IGYWC、IGYWCL或IGYWCLG),可以在过程内的COBOL步骤的SYSLIB参数中提供一个DD语句。例如:
1 |
|
示例21. JCL SYSLIB语句
这将告诉编译器在提供的数据集上查找复制本。
让我们看一个如何在程序中使用COPY语句的示例。
假设一个名为DOWORK的复制本存储了以下语句:
1 |
|
示例22. DOWORK复制本的内容
我们可以使用COPY语句来检索复制本:
1 |
|
示例23. 基本COPY
DOWORK过程中的语句将在DISPLAY语句之后执行。
与子程序不同,使用复制本不会将控制权转移到另一个程序。但是复制本中编写的代码只会在编译期间传输一次。因此,对复制本的进一步更改将需要重新编译程序。
另一方面,子程序中的代码只会在程序执行期间被调用。因此,假设子程序是动态链接的,我们可以在不需要重新编译调用程序的情况下进行更改。
总结
总之,本章应该提供了理解结构化编程以及它与COBOL的关系以及理解和维护代码的重要性所需的基础。已经提供了许多关于如何、何时以及为什么实施关键技术的示例,并进行了解释以进一步理解。您应该能够识别结构化编程(COBOL)与面向对象编程(Java)之间的基本差异。您还应该了解关于Procedure Division结构的最佳实践的一般概念,包括段落的设计和内容、程序控制选项以及在同一系统中调用其他程序的方式。