现在你已经了解程序的一些基础知识,让我们更仔细的了解如何设计程序。
当你坐下来编写程序时,通常你会有某种想法,并希望为其编写程程序。新程序员常常难以弄清楚如何将这个想法转为实际代码。但事实证明,你已经具备了从日常生活中获得许多解决问题的技能。
要记住的最重要的事情(也是最难做的事情)是在开始编码之前设计程序。在许多方面,编程就像建筑。如果你尝试在不遵循建筑计划的情况下建造一座房子,会发生什么?
除非你非常有才华,天赋异常出众,否则你最终会得到一座很多问题的房子:墙壁不够直、屋顶漏水、窗户位置偏移等等,同样,如果你在没有计划之前就开始编程,你可能会发现你的代码会有很多问题,并且你将不得不花费大量的时间来修复本来可以通过编码前稍微思考就能避免的问题。
从长远来看,一些预先的计划可以节省你的时间和挫败感。在本系列教材中,我们将提出一种将想法转换为简单功能程序的通用方法。
设计步骤1:定义你的目标
为了编写一个成功的程序,你首先应该明确你的目标是什么 理想情况下,你应该可以用一两句话来表达这一点。将其表达为面向用户的结果通常很有用,例如;
允许用户组织姓名和关联电话号码的列表。
生成随机的地下城,这些地下城会产生看起来很有趣的洞穴。
生成高股息股票的股票推荐列表。
模拟从塔上掉落的球到落地所需的时间。
虽然这一步看起来明显又简单,但确非常重要。你能做的最糟糕的事情就是编写一个实际上并没有达到你(或者你老板)想要的效果的程序。
设计步骤2:定义需求
虽然定义问题可以帮助您确定想要的结果,但它仍然很模糊。下一步是考虑需求。
需求是一个花哨的词,既指您的解决方案需要遵守的约束(例如预算、时间线、空间、内存等),也指程序为了满足用户需求而必须展示的功能。
请注意,你的要求同样应该关注“什么样的结论或者结果” 而不是“如何如何实现”,例如:
应保存电话号码,以便以后调用。
随机地下城应该始终包含从入口到出口的方法。
股票推荐应利用历史定价数据。
用户应该能够输入塔的高度。
我们需要在 7 天内提供可测试的版本。
该程序应在用户提交请求后 10 秒内生成结果。
程序应该在不到 0.1% 的用户会话中崩溃。
一个问题可能产生许多要求,只要满足所有要求后,简介方案才算是完成的。
设计步骤3:选择你的工具、目标和备份计划
当你是一名经验丰富的程序员时,此时通常会执行许多其他步骤,包括:
第一你的程序将运行的目标架构或者操作系统。
确定将使用哪组开发工具。
确定程序是你自己开发还是和团队进行协作。
定义你的测试、反馈、发布策略。
确定如何备份代码。
当然,作为一名新程序员,这些问题的答案通常很简单:你正在使用你下载的 IDE 在你自己的系统上独自编写一个供你自己使用的程序,并且你的代码可能不会被除了您之外的任何人使用。 。这让事情变得简单。
也就是说,如果你要处理任何非常复杂的事情,应该制定一个备份代码的计划。仅将目录压缩或复制到计算机上的另一个位置是不够的(尽管这比没有好)。
如果你的系统崩溃,你将失去一切。因此一个好的备份策略包括从系统中获取一份代码副本。
有很多简单的方法可以做到这一点:将其压缩并通过电子邮件发送给自己,将其上传到云存储服务(例如 Dropbox),使用文件传输协议(例如 SFTP)将其上传到另一台计算机,将其复制到本地网络上的另一台计算机,或使用驻留在另一台计算机或云中的版本控制系统(例如 github)。 版本控制系统具有额外的优势,不仅能够恢复文件,还能够将其回滚到以前的版本。
设计步骤4:将困难问题分解为简单问题
在现实生活中,我们经常需要执行非常复杂的任务,试图弄清楚如何完成这些任务可能非常具有挑战性。在这种情况下,我们经常采用自上而下的方法来解决问题。也就是说,我们不是解决单个复杂的任务,而是将该任务分解为多个子任务,每个子任务单独更容易解决。如果这些子任务仍然太难解决,可以进一步分解。
通过不断地将复杂的任务分解为更简单的任务,最终可以达到每个单独的任务都可以管理的程度 ;
让我们看一个例子。假设我们想打扫房子。我们的任务层次结构目前如下所示:
- 打扫房子
打扫整个房子是一项相当大的任务,需要一次完成,所以让我们把它分成几个子任务:
打扫房子
用吸尘器吸地毯
清洁浴室
打扫厨房
这更易于管理,因为我们现在可以单独关注子任务。然而,我们可以进一步细分其中一些:
打扫房子
用吸尘器吸地毯
清洁浴室
擦洗马桶
擦洗水槽
打扫厨房
清理台面
清洁台面
擦洗水槽
整理垃圾
现在我们有一个任务层次结构,没有一个任务特别难。通过完成这些相对容易管理的子项目中的每一个,我们就可以完成更困难的打扫房子的整体任务。
创建任务层次结构的另一种方法是自下而上。在此方法中,我们将从简单任务列表开始,并通过对它们进行分组来构建层次结构。
事实证明,这些任务层次结构在编程中非常有用,因为一旦有了任务层次结构,您就基本上定义了整个程序的结构。
顶级任务(在本例中为“打扫房子”)变为 main() (因为这是您要解决的主要问题)。子项成为程序中的函数。
如果发现其中一项(功能)太难实现,只需将该项目拆分为多个子项/子功能即可。最终,您应该达到程序中的每个功能都可以轻松实现的程度。
设计步骤5:弄清楚事件的顺序
现在你的程序已经有了结构,是时候确定如何将所有任务链接在一起了。第一步是确定将执行的事件的顺序。例如,当你早上起床时,你会按照什么顺序执行你上班的任务?
它可能看起来像这样:
卧室用品
浴室用品
早餐的东西
交通相关
如果我们正在编写一个计算器程序,我们可能会按照下面的顺序执行操作:
获取用户第一个输入的数字
获取用户的数学运算符
获取用户第二个输入的数字
计算结果
打印结果
至此,我们已经做好了实施的准备。
实施步骤1:概述你的主要职能
现在我们准备开始实施。上述序列可用于概述您的主程序。暂时不用担心输入和输出。
int main()
{
// doBedroomThings();
// doBathroomThings();
// doBreakfastThings();
// doTransportationThings();
return 0;
}
或者对于计算器程序来说:
int main()
{
// Get first number from user
// getUserInput();
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}
实施步骤2:实现各个功能
在此步骤中,对于每个函数,你将执行三件事:
定义函数原型(输入输出)
编写函数
测试功能
如果你的函数足够精细,那么每一个函数都应该相当简单明了。
如果给定的功能看起开仍然过于复杂,那么可能需要将其分解为更加容易实现的子功能,或者你可能以错误的顺序执行某些操作,并且需要重新审视事件的顺序。
让我们执行计算器实例中的第一个函数:
#include <iostream>
// Full implementation of the getUserInput function
int getUserInput()
{
std::cout << "Enter an integer: ";
int input{};
std::cin >> input;
return input;
}
int main()
{
// Get first number from user
int value{ getUserInput() }; // Note we've included code here to test the return value!
std::cout << value << '\n'; // debug code to ensure getUserInput() is working, we'll remove this later
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}
首先,我们确定getUserInput函数不带参数,并将向调用者返回一个 int 值。这反映在返回值为 int 且没有参数的函数原型中。接下来,我们编写了函数的主体,即简单的 4 条语句。
最后,我们在main函数中实现了一些临时代码,以测试函数getUserInput (包括其返回值)是否正常工作。
我们可以使用不同的输入值多次运行该程序,并确保该程序此时的行为符合我们的预期。如果我们发现某些东西不起作用,我们就知道问题出在我们刚刚编写的代码中。
一旦我们确信程序到目前为止按预期工作,我们就可以删除临时测试代码,并继续实现下一个函数(函数getMathematicalOperation )。
请记住:不要一次性实施整个计划。分步骤进行,在继续之前测试每个步骤。
实施步骤3:最终测试
一旦你的程序“完成”,最后一步就是测试整个程序并确保它按预期工作。
编写程序时的建议
让你的程序易于启动
新程序员通常对他们希望程序完成的所有事情都有一个宏伟的愿景。 “我想编写一款带有图形和声音以及随机怪物和地牢的角色扮演游戏,你可以访问一个城镇来出售你在地牢中找到的物品”。
如果你试图写一些太复杂而无法开始的东西,你会因为缺乏进展而感到不知所措和沮丧。相反,让你的第一个目标尽可能简单,并且绝对是你力所能及的。
随着时间的推移添加功能
旦你的简单程序运行并且运行良好,你就可以向它添加功能。例如,一旦您可以显示您的字段,请添加一个可以四处走动的角色。一旦您可以四处走动,请添加可能阻碍您前进的墙壁。
一旦有了围墙,就可以用它们建造一个简单的城镇。一旦你有了一个城镇,就可以添加商人。通过逐步添加每个功能,你的程序将变得越来越复杂,而不会在此过程中让您感到不知所措。
一次专注于一个领域
不要尝试一次编写所有内容,也不要将注意力分散到多个任务上。一次专注于一项任务。拥有一项工作任务和五个尚未开始的任务比六个部分工作的任务要好得多。
如果你分散注意力,你就更有可能犯错误并忘记重要的细节。
边做边测试每一段代码
新程序员通常会一次性编写整个程序。然后,当他们第一次编译它时,编译器报告了数百个错误。这不仅令人生畏,而且如果您的代码本身存在问题,可能很难找出原因。
相反,编写一段代码,然后立即编译并测试它。如果不起作用,你就会确切地知道问题出在哪里,并且很容易修复。一旦确定代码可以工作,请移至下一部分并重复。
完成代码的编写可能需要更长的时间,但是当您完成后,整个事情应该可以正常工作,并且您不必花费两倍的时间来尝试找出为什么不能正常工作。
不要执着于完善早期代码
功能(或程序)的初稿很少是好的。应该随着你添加功能并找到更好的方法来构建它,程序往往会随着时间的推移而不断完善。
如果你过早的执着于完善代码(添加大量文档、完全遵循最佳实践、优化每一个细节),那么当需要更改代码时,你可能会面临失去所有成果的风险;
优化可维护性,而不是性能
唐纳德·高德纳 有一句名言:“过早的由于是万恶之源。”新程序员经常花费太多时间思考如何对他们的代码进行微观优化(例如试图找出两个语句中哪一个更快)。这不是此时的重点。
大多数性能优势来自于良好的程序结构、针对当前问题使用正确的工具和功能以及遵循最佳实践。应该使用额外的时间来提高代码的可维护性。找到冗余并删除它。
将长函数拆分为较短的函数。用更好的代码替换笨拙或难以使用的代码。最终结果将是代码更容易在以后改进和优化(在确定实际需要优化的地方之后)并且错误更少。
预先花一点时间思考如何构建程序将带来更好的代码,并减少查找和修复错误所花费的时间。
当您对这些概念和技巧越来越熟悉时,它们就会变得更加自然。最终,你将达到可以用最少的预先规划编写整个函数(和短程序)的程度。