(十)模块化开发 -- 1. Pig Latin:一个模块化开发的案例研究

1. Pig Latin:一个模块化开发的案例研究

  • 模块化开发(modular development)
    把一个程序分成多个模块的技术 。

为了阐述这种技术,本章给出了一个利用模块化开发能够提供明显优势的问题。

这个问题是写一个程序用来从终端读入一行文本,并把这行文本中的英文转换成Pig Latin。


Pig Latin是按照如下简单规则转换每个英文单词的一种自发明语言。

(1) 如果单词以辅音开头,那么把起始辅音字符串(即直到第一个元音字母的所有字母)从单词开始移到单独尾部,并加上后缀ay。

(2) 如果单词以元音开头,则加后缀way。

例如,假设单词是scram,以辅音开头,这样就创建了Pig Latin单词amscray;
对于以元音开头的单词,如apple,只要在末尾加上way,从而得到appleway。


1.1 应用自顶向下的设计

既然问题是把整行英文翻译成Pig Latin,那么,程序应该能够生成如下运行示例:

通常,仅仅在比较了不同的解决方法以后,才可以决定一个程序是否需要一个特定模块分解。

因此,最好的方法常常是先应用「自顶向下」的设计方法:
从主程序的层次着手,然后依次写下一系列函数,每个函数解决整个问题中的一个部分。

在初始阶段,可以用一系列有待具体实现的高层步骤来定义main函数。通常,先描述出其功能。
如,初稿为:

  • 伪代码(pseudocode)
    由英文和C语言混合构成的程序。

1.2 使用伪代码

伪代码的作用在于,记录逐步精化的过程。

在用一系列英文步骤写出整个程序之后,可以再回到伪代码语句,用实际的C语言代码来实现它们。

在用代码替换伪码的过程中,对于难编码的部分,最好的方法是实施逐步精化的策略:
用一个效果相当的新的函数来替换这行伪代码。

例如,1.1中的main函数的实现如下:

#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"

main () {
    string line;
    line = malloc(1000);
    printf("Enter a line: ");
    scanf("%[^\n]", &line[0]);
    TranslateLine(line);
}

TranslateLine定义描述和原型:

/* 
* Function: TranslateLine
* Usage: TranslateLine(line);
* ---------------------------
* This function takes a line of text, translates it to Pig Latin and displays the result.
*/

void TranslateLine(string line);

1.3 实现TranslateLine

实现TranslateLine,需要进一步把它分解。

在大多数情况下,没有一个特定的分解策略是绝对正确的。
因此,通常需要考虑几种分解问题的方法,然后看哪一种策略最好。

实现TranslateLine时,需要先解决如何把一个字符串分为单词,把每个单词翻成Pig Latin,然后在屏幕上显示每个Pig Latin单词的问题。

根据这个问题的陈述,可以写出如下的概念分解:

理论上分解合理,但会导致某些实际的问题:
在第一步中,将语句行分解为单词,如何存储结果呢?实现这个概念的函数返回的不是一个单词,而是一个单词列表。

仔细考虑会发现,不需要立即记录所有的单词。一旦找到一个单词,就可以马上翻译并且正确显示。一旦显示出这个单词,就可以丢开它继续处理下一个单词。

由此,可以提出第二种策略:


1.4 考虑空格和标点符号的问题

上述伪代码版本所用的策略中有个小问题:没有把空格和标点符号考虑进去。

程序设计中的一个现实问题是问题的描述通常是不完整的。在某些情况下,被省略的部分是非常重要的。在很多时候,必须自己选择一种合理的策略。

判断什么是合理的策略也有一些技巧。

在这个例子中,因为标点符号和空格都能表达出某种意义,所以最好让它们在输出中的位置和在输入中的位置相同。

因此,需要的输出为:

可以用许多方法重新设计该程序,使得标点符号正确地出现在输出中。

例如,一种方法是改变主循环,使得它按字符而不是按单词为单位来处理。

如果使用这种策略,伪代码可以按如下结构实现:

不过,在分解中暴露出一个更严重的问题:
前一节显示出来的伪代码版本包含的语句:

Get the next word.

在刚才的版本中消失了。

如果从程序员的角度考虑,会认为“get the next word”这个操作是一个非常有用的工具,它的应用范围远远超出简单Pig Latin程序。许多问题都需要把文本分割成单词。如果能开发出一个通用的函数来执行这个操作,那么将来解决此类问题都将从此受益。

另一方面,要用到当前的应用中,返回下一个词的函数还必须能够返回空格和标点符号,以使输出行中能包括这些符号。


1.5 精化单词的定义

当前需要做的是对一个单词的概念进行精化。如果看到这样的输入行:

this is pig latin.

可以将这个输入行看作由以下8个单独的片段组成:

和单词一样,上述方法把空格和标点解释为单独的实体。

  • 记号(token)
    在计算机科学中,作为一个相关单元的字符序列被称为一个记号。

上图中,每个方框都表示一个记号。


把空格和标点符号作为独立的记号使得可以修改TranslateLine这个方案,使这些符号也显示出来。

修改过的伪代码策略如下:

把一行分成一些独立的记号的想法在计算机科学中非常常见。

例如,当C编译器将一段程序翻译成机器代码的时候,此过程的第一步就是将输人文件分割成C语言所用的记号,即变量名、数字、运算符等等。

  • 词法分析(lexical analysis)/记号扫描(token scanning)
    将输入分解成记号的过程称为词法分析,或者记号扫描。

1.6 设计记号扫描器

要使用新的策略来完成TranslateLine的实现,必须首先设计能够分割输入行的记号扫描器。

由于它很可能是个通用的工具,因此,将记号扫描器设计成一个独立的模块就十分有意义。

在前一节最新的TranslateLine伪代码版本中,扫描器模块有两个不同的作用:
第一,扫描器必须提供一个函数返回当前行中的下一个记号,可以命名为GetNextToken
第二,当扫描完最后一个记号时要让客户知道,一种选择是定义一个叫AtEndOfLine的谓词函数,它在读到最后一个记号时返回TRUE


必须将扫描器模块设计为能够追踪划分行的进度:
GetNextToken从行返回一个记号后,必须记住已经扫描过的那些记号,以便在下次调用时返回一个不同的结果。

  • 内部状态(internal state)
    一个模块中的多次函数调用之间的信息称为内部状态。

当模块维护内部状态时,模块的接口通常导出一个用来初始化此状态信息的函数。
例如,在扫描器模块中,提供一个函数InitScanner(line),它使得客户能够告诉扫描器在字符串line的开头开始返回记号。为了得到这些记号,客户只要调用GetNextToken即可,不需要参数。

哪个记号该被返回的信息是扫描器模块要维护的内部状态的一部分:
第一次调用GetNextToken返回第一个记号,下次调用返回第二个记号,如此继续直到所有的记号都被读取为止。


现在,可以用函数InitScannerGetNextTokenAtEndofLine来更新TranslaterLine的实现。如下所示:


1.7 完成TranslateLine的实现

如果继续进行逐步精化的策略,可以用函数调用替换剩下的英文语句来完成这个实现。

因此,完成后的TranslaterLine是:

void TranslateLine(string line) {
    string token;
    InitScanner(line);
    while (!AtEndOfLine()){
        token = GetNextToken();
        if (IsLegalWord(token)) token = TranslateWord(token);
        printf("%s", token);
    }
    printf("\n");
}

然而,这个解决方案中还有两个函数仍然没有实现:IsLegalWordTranslateWord


谓词函数IsLegalword确定GetNextToken返回的记号是一个需要转换成Pig Latin的单词还是一个简单的标点符号。

Pig Latin规则仅当一个单词全部由字母组成时才有意义。因此,如果token中的每个字符都是字母,IsLegalWord(token)返回TRUE

代码实现如下:

bool IsLegalWord(string token) {
    int i;
    for (i = 0; i < StringLength(token); i++)
    {
        if (!isalpha(IthChar(token, i)))
            return false;
    }
    return true;
}

在伪代码中,TranslateWord的结构对应了Pig Latin的规则。

函数FindFirstVowel(word)返回word的第一个元音的下标位置。

这样,在TranslateWord实现中的第一条语句,可以写成如下形式:

vp=FindFirstVowel(word);

其中,vp是个用来记录元音位置的整型变量。


那么,如果word中没有元音怎么办?

库函数FindCharFindString使用-1作为一个特殊的标志,用来指出函数查询的值在字符串中不存在。这里也可以采用相同的策略。

FindFirstVowel返回-1时,简单的办法是让TranslateWord返回原来的单词。


由此,TranslateWord的代码实现如下:

string TranslateWord(string word) {
    int vp;
    string head, tail;
    vp = FindFirstVowel(word);
    if (vp == -1) {
        return word;
    } else if (vp==0) {
        return Concat(word, "way");
    } else {
        head = SubString(word, 0, vp - 1);
        tail = SubString(word, vp, StringLength(word) - 1);
        return Concat(Concat(tail, head), "ay");
    }
}

其中,FindFirstVowel的实现为:

int FindFirstVowel(string word) {
    int i;
    for (i = 0; i < StringLength(word); i++) {
        if (IsVowel(IthChar(word, i))) return i;
    }
    return -1;
}

bool IsVowel(char ch) {
    switch (tolower(ch)) {
        case 'a': case 'e': case 'i': case 'o': case 'u':
            return true;
        default:
            return false;
    }
}

1.8 定义扫描器模块接口

扫描器模块的接口,要输出三个函数:InitScannerGetNextTokenAtEndofLine

产生scanner.h接口的工作主要包括给每个函数编写注释,如下图所示:





参考
《C语言的科学和艺术》 —— 第10章 模块化开发

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值