设计第一个程序
本节阅读量:我们已经学习了一些程序的基础知识,现在来更仔细地看看如何设计程序。
通常,当有某种想法并想通过程序实现时,新手程序员很难弄清楚如何将想法转换为实际的代码。但事实上,你已经具备了许多所需的技能。
最重要的事情(也是最难做的事情)是在开始编码之前设计好程序。很多时候,编程就像盖房子。如果试图在没有建筑图纸的情况下建造房子,会发生什么?除非你很有天赋,否则最终会得到一个有许多问题的房子:墙不直、屋顶漏水等等。类似地,如果在有一个好的计划之前就尝试编程,很可能会发现代码有许多问题,你将不得不花费大量时间来解决那些本可避免的问题。
长远来看,提前计划将节省时间,减少挫败感。
在本课中,将展示一种将想法转换为简单程序的通用方法。
设计步骤1:确定目标
为了编写成功的程序,首先需要确定目标。理想情况下,应该能用一两句话来说明。例如:
- 用户所在的组织名称和电话号码的列表。
- 随机生成地下城和有趣的外观洞穴。
- 生成具有高股息的股票推荐列表。
- 模拟球从塔上掉落到地面所需的时间。
尽管这一步似乎很明显,但也非常重要。最糟糕的情况是,编写了一个与实际想要的不同的程序!
设计步骤2:确定需求
虽然步骤1可以帮助确定想要的结果,但仍然是模糊的。下一步是考虑需求。
需求是一个术语,用于表示解决方案需要遵守的约束(例如预算、时间、空间、内存等),以及程序为满足用户需求而必须具备的功能。注意,需求同样是关注”什么”,而不是”如何”。
例如:
- 应保存电话号码,以便以后调用。
- 随机副本应始终包含从入口到出口的路线。
- 股票推荐应依据历史定价数据。
- 用户应能够输入塔的高度。
- 需要在7天内出测试版本。
- 程序应在用户提交请求后10秒内生成结果。
单个问题可能会产生许多需求,解决方案直到满足所有需求时才“完成”。
设计步骤3:确定工具、目标和代码备份
对于经验丰富的程序员,此时通常会有许多其他步骤,包括:
- 定义程序将在其上运行的目标体系结构或操作系统。
- 确定要使用的工具集。
- 决定是单独编写程序还是作为团队共同编写程序。
- 定义测试/反馈/发布策略。
- 确定如何备份代码。
然而,对于新手程序员,这些问题的答案通常很简单:用正在使用的购买或下载的IDE,独自在自己的系统上编写供自己使用的程序,而且代码可能除了自己之外不会被任何人使用。
如果要处理复杂逻辑,应该有一个备份代码的计划。仅仅将目录压缩或复制到机器上的另一个位置是不够的(尽管这总比没有要好)。如果系统崩溃,你将失去一切。一个好的备份策略需要将代码的完整副本保存到系统之外。有许多简单的方法可以做到:将代码压缩后通过电子邮件发送给自己,将其复制到百度云或其他云服务,用FTP传到另一台计算机,将其拷贝到本地网络上的另一台机器,或使用安装在另一台机器或云上的版本控制系统(例如GitHub)。版本控制系统还有一个额外的优势:不仅能恢复文件,还能回滚到以前的版本。
设计步骤4:将困难问题分解为简单问题
在现实生活中,经常需要执行非常复杂的任务。试图一步到位地完成这些任务是非常有挑战性的。在这种情况下,常用的方法是自顶向下的问题解决法。即不是直接解决单个复杂的任务,而是将该任务拆解为多个子任务,每个子任务都更容易单独解决。如果这些子任务仍然难以解决,则可以进一步细分。通过不断地将复杂任务拆分为简单任务,最终可以达到每个单独的任务都是可管理的程度。
例如,假设想打扫房子。任务层次结构如下所示:
- 打扫房子
一次清洁整个房子是一项相当大的任务,所以让我们将其分解为子任务:
- 打扫房子:
- 清理客厅
- 清理厨房
- 清理卫生间
这更容易管理了,因为有了可以单独关注的子任务。不过,还可以进一步细分:
- 打扫房子:
- 清理客厅
- 打扫地面
- 清理厨房
- 清理垃圾
- 洗餐具
- 清理卫生间
- 清洁马桶
- 整理洗漱用具
- 清理客厅
现在的任务层次结构中,没有一个任务特别困难。通过完成这些相对容易管理的子任务,就能完成更困难的"打扫房子"总体任务。
创建任务层次结构的另一种方法是自下而上。在这种方法中,从简单任务的列表开始,通过对它们进行分组来构造层次结构。
例如,许多人在工作日要上班或上学,假设想要解决”上班”的问题。如果有人问你早上起床上班需要做什么,你可能会得出以下列表:
- 挑选衣服
- 穿上衣服
- 吃早餐
- 出发上班
- 刷牙
- 起床
- 准备早餐
- 取自行车
- 洗脸
使用自下而上的方法,将具有相似性的任务分组在一起,组织成层次结构:
- 起床
- 关闭闹钟
- 下床
- 穿衣服
- 清洁自己
- 刷牙
- 洗脸
- 早餐
- 备餐
- 吃早饭
- 路上
- 取自行车
- 骑自行车
这样的任务层次结构在编程中非常有用,因为有了任务层次结构后,就基本上定义了整个程序的结构。顶级任务(在本例中,”打扫房子”或”上班”)相当于main()函数(因为它是要解决的主要问题)。子任务则成为程序中的函数。
如果其中某一项(功能)太难实现,只需将该项拆分为多个子项/子功能。最终应该达到这样的效果:程序中的每个函数都很容易实现。
设计步骤5:确定事件的顺序
现在程序有了结构,是时候确定如何将所有任务链接在一起了。第一步是确定事件的执行顺序。例如,早晨起床时,上述任务的执行顺序可能如下:
- 起床
- 清洁
- 早餐
- 上路
如果是编写计算器,可以按以下顺序执行操作:
- 用户输入第一个数字
- 用户输入数学运算符
- 用户输入第二个数字
- 计算结果
- 打印结果
到这一步,设计完成后,我们就已经为实际实现做好了准备。
实施步骤1:概述主要功能
现在准备开始编写代码。上面的序列可以用于概述主程序的框架。暂时不要担心输入和输出。
|
|
或者对于计算器:
|
|
请注意,如果要使用这种”大纲”方法来构造程序,函数将无法编译,因为函数定义尚不存在。在准备好实现函数定义之前,注释掉函数调用是解决此问题的一种方法。或者,可以先定义对应的空函数,以便程序可以编译通过。
实施步骤2:实现每个函数
在这一步中,对于每个函数,将执行三项操作:
如果函数的粒度足够细,则每个函数都应该相当简单和直接。如果某个函数看起来仍然过于复杂、难以实现,则需要将其分解为更容易实现的子函数(或者,如果事件的顺序定义有误,需要重新检查)。
下面来实现计算器示例中的第一个函数:
|
|
首先,确定getUserInput函数不接受参数,并将向调用方返回一个int值。这在返回值为int且没有参数的函数原型中得到体现。接下来,我们编写了函数体,包含4条语句。最后,在函数main中实现了一些临时代码,用来测试函数getUserInput(包括其返回值)是否正常工作。
使用不同的输入值多次运行该程序,确保程序的行为符合预期。如果发现某些情况不正常,就可以知道问题出在刚刚编写的代码中。
一旦确认程序这部分按预期工作,就可以删除临时测试代码,继续实现下一个函数(函数getMathematicalOperation)。
记住:不要一次性实现整个程序。分步骤进行,在继续下一步之前测试每个步骤。
实施步骤3:最终测试
一旦程序“完成”,最后一步是测试整个程序,并确保按预期工作。如果不工作,请修复它。
编写程序时的建议
让程序易于起步。新手程序员通常对他们的程序有宏伟的愿景。”我想编写一个具有图形和声音、随机怪物和地牢的角色扮演游戏,有一个你可以访问的城镇来出售在地牢中找到的物品”。如果你试图写一些太复杂的东西,你会因缺乏进展而不知所措、倍感沮丧。相反,让你的第一个目标尽可能简单,是你力所能及的。例如,”希望能在屏幕上显示一个场景”。
随着时间的推移逐步添加功能。一旦简单程序工作正常,就可以向其添加功能。例如,一旦可以显示场景,就可以添加一个可以四处走动的角色。一旦可以四处走动,就添加可能阻碍前进的墙壁。一旦有了墙壁,就用它们建造一个简单的城镇。一旦有了城镇,就添加商人。通过逐步添加每个功能,程序将逐渐变得更复杂,而不会在过程中难倒你。
一次专注于一个领域。不要试图一次编写所有代码,也不要将注意力分散在多个任务上。一次专注于一项任务即可。一个完成的任务加上五个尚未开始的任务,比六个半完成的任务要好得多。分散注意力,很可能会犯错误、忘记重要的细节。
边写边测试每段代码。新手程序员通常会一次性编写整个程序。当第一次编译时,编译器报出数百个错误。这不仅令人望而却步,如果代码不能工作,也很难找出原因。相反,应该编写一段代码,然后立即编译和测试它。如果不工作,能确切地知道问题在哪,也更容易修复。一旦确认代码可以工作,就继续下一部分。这样完成代码编写也许需要更长时间,但当完成时,整个程序可以正常工作,不必花费数倍的时间来排查问题。
不要沉迷于完善早期代码。函数(或程序)的初稿很少是完美的。此外,随着添加功能并找到更好的构建方法,程序往往会随着时间的推移而改变。如果过早地沉迷于改进代码(添加大量文档、完全遵守最佳实践、进行优化),当需要更改代码时,之前的投入可能就白费了。相反,让功能先能最基本地运行,然后继续下一步。当对解决方案充满信心时,再进行代码重构。不要追求完美——有价值的程序永远都不是完美的,总有可以改进的地方。达到”足够好”就继续前进。
优化可维护性,而不是性能。Donald Knuth有一句名言:”过早的优化是一切罪恶的根源”。新手程序员通常花费太多时间考虑如何微观优化代码(例如,试图找出两个语句中哪一个更快)。这很少有实际意义。大多数性能优势来自良好的程序结构、使用正确的工具和功能来解决手头的问题,以及遵循最佳实践。应该把额外的时间用来提高代码的可维护性。找到冗余并删除。将长函数拆分为短函数。用更好的代码替换笨拙或难以使用的代码。最终的结果是代码更容易在以后进行改进和优化(在确定了实际需要优化的位置之后),并且错误更少。在问题变严重之前就发现它们。
总结
许多新手程序员简化了设计过程(因为它看起来需要额外的精力,或者不像编写代码那么有趣)。然而,对于任何有价值的项目,从长远来看,遵循这些步骤将节省大量时间。预先做好设计可以减少后期大量的调试工作。