Justin's Words

怎么设计一个程序

翻译自:Learn Cpp.Com
 

一般你打算写一个程序时,想必是为了解决某种问题或者是为了模拟某种状态,不过相对新程序员来说,该怎么把这些想法转换成实实在在的代码呢?而事实上你已经从日常生活中获得很多解决问题的方法(只是不知怎么应用到程序上)。

最重要的也是最难的就是在你开始码代码之前,你应该先设计好你的程序。从很多方面来讲,编程就像建筑,相应来说,如果你还没一个建设计划就开始建筑一栋房子,除非你相当聪明,否则你的房子必然有一大堆问题:屋顶漏水或墙壁不直等。同样的,还没有个精心策划就开始写代码的话,那么在此过程中你就得花一大堆时间处理编程过程中出现的各种因为缺乏预先设计而出现的问题。

所以说,在编程中,一个精心的计划会大大减少你的时间,同时避免很多不必要的挫折。

第一步:定义问题 (Define the problem)

首先你得有个明确定义,就是你这个程序是为了解决什么问题的。最理想的做法是你应该可以把这些需求用几个句子写下来,比如:

  • 我想做个联系人的 App 来帮我存储朋友们的电话/手机号码
  • 我想做个随机生成地牢的生成器来生成各种好玩的洞穴(用于游戏)
  • 我想做个程序来获取股票信息,同时预测我该买哪只股票
    虽然这看起来很明显,但还是非常重要的,(如果没定义好问题)最差的情况就是,你写了个违背你或你的老板当初意愿的程序,白费力气。

第二步:定义目标(Define your targets)

如果你是一位经验丰富的程序员,在这一步会有很多要点,包括:

  • 你的目标用户是什么人
  • 你的程序要运行在什么架构或系统上
  • 是自己写还是加入团队一起写
  • 收集需求(有一个文档性的列表列出程序所要处理的需求)
    然而对于一个程序员新手来说,这些问题的答案都很简单,那就是这个程序是给自己用的,自己来写,在自己当前的系统上运行,使用自己买来的或者下载的 IDE,那这样事情就简单多了,那我们就没必要花过多时间在这一步上了。

第三步:做个任务完成结构(Make a heirarchy of tasks)

在现实生活中,我们经常要把很复杂的任务直接用文字表现出来,然后找出比较难处理的任务,在这种情况下,我们就可以分清任务的主次,重要的和不重要的。对过于复杂的部分,我们可以将其分解开来作为不同的子任务,分而处理,这样就简单多了。如果分开的子任务处理还是麻烦,那就继续将其分开为子任务。全部分解完成后,那么各个子任务就非常明了了,同时易于管理如果不是那么琐碎。

举个例子,我们要写篇有关萝卜的报道,那我们的最简单任务结构就如下:

  • 写篇有关萝卜的报道
    明显这个任务太笼统了,只用一句话来表述明显不够的,分解成子任务看看:

  • 写篇有关萝卜的报道

    • 调查萝卜的相关问题
    • 写概要
    • 完善概要有关萝卜的细节的部分
      这样就便于管理多了,但第一个子任务 “调查萝卜的相关问题” 还是太模糊,那就再分解下:
  • 写篇有关萝卜的报道

    • 调查萝卜的相关问题

      • 去图书馆借来有关萝卜的书籍
      • 上网搜索有关萝卜的信息
    • 写概要

      • 关于萝卜的生长(growing)
      • 关于萝卜的处理(processing)
      • 关于萝卜的营养(nutrition)

      • 完善概要有关萝卜的细节的部分

那我们这个任务结构就完成了,因为没有那个是特别(particularly)难得,我们可以通过完成各个相对易于管理的子任务来完成很难的任务,从而完成整个任务。

另一种方法就是自下而上来列出各个任务来完成整个任务结构。这种方法就是先列出一大堆容易的任务,然后给它们分组。

举个例子,早上你要去上学或上班,那从你起床到出门这期间你得完成很多任务,列个清单看看:

  • 拿出衣服
  • 穿好衣服
  • 吃早餐
  • 开车去上班
  • 刷牙
  • 起床
  • 准备早餐
  • 上车
  • 洗澡

我们按照从下而上的方法来把这些人物组织起来看看,同时给相近的任务分好组:

  • 从起床到去上班

    • 在卧室完成的

      • 起床
      • 拿出衣服

      • 在浴室完成的

        • 洗澡
        • 刷牙
      • 早餐相关

        • 准备早餐
        • 吃早餐
      • 交通相关

        • 上车
        • 开车去上班

这些结构完成后,对编程的帮助是相当大的,因为这些结果能让你本质上定义整个程序的结构,最顶级的任务(在上面的例子就是 “写篇有关萝卜的报道” 或 “从起床到去上班”)则是 main() (因为这是你要解决的主要条款),而子任务就是 main() 内的各个函数。

如果你发现某个子任务 (函数)还是难以执行,那就把这个子任务再分解成子任务,然后通过让这个函数调用子函数来执行这个任务,就算这会让你的程序变得很琐碎你还是应该这样做。

第四步:列好任务处理的顺序

有了你的程序结构后,就得开始把这个任务连贯起来,首先列好任务处理的顺序,套用上面起床的例子,这些任务的顺序就像如下:

  1. 起床
  2. 拿出衣服
  3. 洗澡
  4. 穿好衣服
  5. 准备早餐
  6. 吃早餐
  7. 刷牙
  8. 上车
  9. 开车去上班

那又比如我们要写个计算的程序,这个程序的任务处理就如下:

  1. 获取用户输入的数字
  2. 获取用户使用的算法
  3. 获取用户再次输入的数字
  4. 计算结果
  5. 输出结果

起床的例子转化为代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
GetOutOfBed();
PickOutClothes();
TakeAShower();
GetDressed();
PrepareBreakfast();
EatBreakfast();
BrushTeeth();
GetInCar();
DriveToWork();
}

计算的例子转化为代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
// 获取用户输入的数字
GetUserInput();

// 获取用户使用的算法
GetMathematicalOperation();

// 获取用户再次输入的数字
GetUserInput();

// 计算结果
CalculateResult();

// 输出结果
PrintResult();
}

第五步:获取每个任务输入的数据和输出的数据

任务结果和顺序都搞定了,接下来就得获取每个人物输入和输出的数据了。如果你从上一步获得了数据,那么这个数据就会成为参数。如果你通过某个函数计算出了一个结果,那这个结果就会作为 return value 存在。

上面的步骤完成后,那每个函数都应该有了原型(prototype)了。忘了?好吧,函数原型(function prototype)就是包括函数名称、参数、返回类型的函数声明,但不会运行这个函数。

看下例子。GetUserInput() 顾名思义,获取用户输入的数字并将其返回给其他函数调用,它的原型如下:

1
int GetUserInput();

在计算的例子中,CalculateResult() 函数则需要 3 次输入:2 次数字输入和 1 次算法符号输入。在这个函数被调用时我们因为已经获得这三次输入,并将这 3 次输入的返回值作为这个函数的参数,然后它会计算出结果,但它不会自己显示结果,所以我们需要它将结果作为返回值给其他函数使用。

那么 CalculateResult() 函数的原型就如下:

1
int GetUserResult(int nInput1, char chOperator, int nInput2);

第六步:写出任务细节

这一步就开始真正执行程序了。如果你已经把总的任务分解成足够小的部分,务必保证每个任务足够简单和直接,如果某个子任务还是过于复杂,那就表明你还得继续对它进行分解直到它能够足够容易地来运行。

比如:

1
2
3
4
5
6
7
8
9
10
11
char GetMathematicalOperation()
{
    cout << "Please enter an operator (+,-,*,or /): ";

    char chOperation;
    cin >> chOperation;

    // What if the user enters an invalid character? 如果用户输入了非法字符呢?
    // We'll ignore this possibility for now 先别管它
    return chOperation;
}

第七步:把输入数据和输出数据联系起来

最后,不管你用什么方法在每个任务(函数)内把输入数据和输出数据联系起来都是可以的。比如你把 CalculateReturn() 输出的值传给 PrintResult() 让它把计算结果打印出来,这时就需要一个临时变量来存储这个输出值,然后通过这个临时变量在两个函数之间传递。

1
2
3
4
// nResult is a temporary value used to transfer the output of CalculateResult()
// into an input of PrintResult()
int nResult = CalculateResult(nInput1, chOperator, nInput2);
PrintResult(nResult);

这种方法往往比不使用临时变量的方法更具有可读性(readable)。
PrintResult( CalculateResult(nInput1, chOperator, nInput2) );
像以上的做法就经常让新手费解。

完成

整个完成的计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "stdafx.h"
#include
#include
using namespace std;

int GetUserInput()
{
cout << "Please enter an integer: ";
int nValue;
cin >> nValue;
return nValue;
}

char GetMathematicalOperation()
{
cout << "Please enter an operator (+,-,*,or /): ";
char chOperation;
cin >> chOperation;
// What if the user enters an invalid character?
// We'll ignore this possibility for now
return chOperation;
}

int CalculateResult(int nX, char chOperation, int nY)
{
if (chOperation=='+')
return nX + nY;
if (chOperation=='-')
return nX - nY;
if (chOperation=='*')
return nX * nY;
if (chOperation=='/')
return nX / nY;

return 0;
}

void PrintResult(int nResult)
{
cout << "Your result is: " << nResult << endl;
}

int main()
{
// Get first number from user
int nInput1 = GetUserInput();

// Get mathematical operation from user
char chOperator = GetMathematicalOperation();

// Get second number from user
int nInput2 = GetUserInput();

// Calculate result
int nResult = CalculateResult(nInput1, chOperator, nInput2);

// Print result
PrintResult(nResult);
}

写程序时的一些建议

  • 刚开始写程序时先别那么复杂,从简单开始。
  • 写程序的过程中不断给它添加特性
  • 写一个部分就专注这个部分,别在其他部分分散精力
  • 进行单元测试