如何编写程序/收集真实经验
良好的实践和生活经验从未被记录在书中。大多数计算机科学书籍都是 API 演练。这本书是不同的!
尝试设置 "一键测试"。这使得它更加方便地输入一些内容,然后按下该按钮,该按钮会
- 保存您刚刚编辑的文件,
- 使用所有适当的选项(如果需要,例如“-fno-emit-frame”)编译应用程序,以及
- 运行一些快速测试,以确保您的“改进”不会意外地破坏其他内容。
花费一个小时来编写一些测试并设置一键测试可能*看起来*比它值得的麻烦更大。手动编译文件,并手动运行应用程序的各个部分以确保它们正常工作,可能需要不到一个小时的时间。但是相信我,大多数程序您将要编辑、编译和测试很多很多次。从现在起一年后,当您只做了一个小小的改动时,您难道不希望按下按钮就完成了吗?而不是
- 手动编译文件
- 手动运行应用程序并发现它突然无法工作
- 拔掉你的头发,直到
- 几个小时后,您想起您需要包含“-fno-emit-frame”
- 手动重新编译文件,这次使用“-fno-emit-frame”
- 从头开始进行测试。
有很多方法可以设置一个 自动构建系统。
一键测试只是某些程序员推荐的持续集成的一部分。
即使律师也能看到自动化构建脚本的优势。 [1]
这不应该只适用于构建脚本。如果您发现自己重复了几行代码,请将其放入脚本或批处理文件中。
如果您是 Windows 程序员,学习使用 Cygwin[2] bash 脚本通常是值得的。它们比 Windows 批处理文件灵活得多。还可以研究 AutoHotKey[3]。
就此而言,不要害怕构建自己的工具!您是一位程序员,如果您正在处理大量元数据,请制作一个元数据编辑器。如果您的构建文件有太多选项难以记忆,而且这些选项经常发生变化,请为它编写一个 GUI。您跳过的每一步都是您不会搞砸的每一步——而且一个单一的搞砸可能会花费您与构建脚本或工具相同的时间。
脚本和工具的另一个原因是知识保留/交流。您可以告诉同事如何构建 4 次,直到他记住了每个步骤,或者您可以将您使用的脚本交给他。Ftorkou besaha
不要使用表锁定!它有缺陷!使用事务,在 JDBC 中设置连接的隔离级别。
始终记住使用 finally 块来关闭资源!否则抛出异常可能会导致许多打开的连接
小心空错误处理程序......错误处理程序永远不应该为空,但这在 Java 中很难强制执行,因为存在已检查的异常。例如,Java 的 sleep 方法抛出一个已检查的异常,因此您必须捕获它。很少有人真正使用它并且不在乎,因此他们用一个空处理程序包装它。正因为如此,已检查的异常被认为是一种反模式或仅仅是错误的。
当堆栈跟踪被隐藏,因为有人认为它们“不重要”或很乱,或者其他人捕获了范围过广的异常并隐藏了本来可以使您的错误修复变得轻而易举的跟踪时,跟踪错误非常困难。
每当您输出任何内容作为错误消息时,请在前面加上例如 new Date() 或类似内容,以便您以后能够跟踪回溯。将模块名称添加到错误消息中也很有用。尝试将错误处理构建为测试能力的一部分。不要仅仅依赖调试器来查找代码错误。
您的时间花在设置 Log4J 等日志框架上是值得的。请注意,某些日志软件配置不适合生产代码,因为日志功能会导致效率损失。对各种设置组合进行一些迭代测试,以了解速度损失,然后决定使用最佳组合。
许多程序员在估计向程序添加某个功能需要多长时间方面很糟糕。练习,练习,再练习。即使没有人问您需要多长时间,也要记录您开始添加该功能的时间、您估计完成的时间以及您实际完成的时间。即使您完全没有头绪,至少也要记下您开始的时间和您停止的时间。每次,您都会学到一些关于完成这类任务需要多长时间的新知识。
令人惊叹的是,有多少程序员就是不写注释,无论多少种不同的 编码标准 建议(甚至强制要求)代码要添加适当的注释。
注释对于软件项目的维护阶段至关重要。任何曾经被要求分析、修改或维护其他人代码的人都会证明,如果没有注释,这项工作比在已正确注释的代码上完成相同任务需要更多精力、时间和金钱。不要忘记,你可能在未来的某个(更晚的)时间点自己承担这项任务。
不要过度注释和不要注释不足同样重要。在每一行代码的末尾添加注释是完全没有必要的。相反,假设读者对该语言有一定的熟悉程度,并将注释限制在描述代码块的功能。
无论何时你在查看代码(即使是自己的代码),如果你对代码有一点不理解,就需要添加注释。如果你对代码很不理解,则需要重构。
你可能会发现,在完成方法的编码后添加注释会更舒服——回到开头,逐个步骤浏览。这也会强制对代码进行一些审查。在编码之前添加注释也是一个好主意——首先在注释中创建一个基本流程,然后在注释之间插入代码。只是不要忘记在你完成后检查一下你的注释,我经常发现当我完成时,我最初的注释中只剩下很少一部分了。
另一个重要的概念是“自文档代码”。这意味着你使用函数、变量、类和方法的名称来传达代码的思想,这样你就不用写太多注释。如果你用它执行的操作来命名一个方法,那么读者就会很清楚它将要做什么;例如,使用名为 determineIfStackIsEmpty()
的方法,而不是只做像 if( size == 0 )
这样的比较。
不要忽视代码模块中的文件头。这些注释对于提供代码操作的摘要以及作者和许可信息非常重要。大多数编辑器程序在屏幕顶部以文件的第一行打开代码模块;使文件的顶部部分包含一个块注释,该注释指定该模块的功能,这在维护期间非常有用。
注释不是一项难以掌握的学科;实际上,创建一个包含多种语言标准注释形式的模板集非常容易,然后在开始一个新模块时复制模板,添加代码和相应的注释细节。一些集成开发环境工具,如 Eclipse 或 X-Code,可以在创建新源文件时自动配置为执行此操作。
最后,在中型到大型的项目中,使用自动文档工具(如 Doxygen、JavaDocs 或 Autodoc)为项目中的模块创建超链接文档可能会有所帮助。这为项目的所有成员提供了相同的标准文档,以便每个人都能了解项目中所有代码模块的可用性和操作细节。
要获得一些可用代码文档的良好示例,请查看库中的 Java 源代码。每个类都有一个注释块,显示一般的类使用情况,每个外部方法都有一个注释块,描述使用情况和注意事项,每个非平凡方法都有内联(非 javadoc)注释,描述代码块(通常是每一行)正在发生的事情。总体而言,每行代码可能会有 5-10 行注释,但方法注释比需要更详细,除非你正在为公共使用构建库。
(当你阅读 Sun 的源代码时,你还会注意到大多数方法只有 1-3 行。良好的 OO 设计看起来像“魔术”——你认为一定有一个巨大的例程在某个地方进行所有“真正的工作”,但你从未见过它,它只是这些只有 1-3 行的例程一直到中心。)
如果你因为输入注释而减慢速度(即使是每行代码 3 行注释),那么你要么急需参加打字课,要么就是打字速度比思考速度快! 请花时间处理你的代码——不要只是草草地写完,然后检查,再继续下一个。在你重构代码的第一遍时,审查并重写注释。你可能认为你通过尽快输入代码更改和错误修复来完成老板的要求,但实际上你是在(根据我的经验)让整个团队因极高的错误数量和可维护性问题而被解雇。
配置管理,也称为“CM”(有时也称为“SCM”代表“软件 CM”),是一个被严重误解的话题。尽管最近态度发生了一些变化,但普遍的看法似乎是,CM 是一种“必要的邪恶”,应该容忍但不要积极参与,至少不应超过绝对必要。
当构建计算机软件时,变化是不可避免的,并且变化会增加正在该项目上工作的开发人员之间的混乱程度。当修改在执行之前没有经过分析、在进行之前没有写下来、没有以提高质量和减少错误的方式进行控制或没有适当地报告给应该了解它们的人员时,就会产生混乱。
需要更改的原因有很多。首先,客户可能会提出一个或多个新的要求,或者可能提出更改现有要求的请求。这可能是由设计审查、应用程序的重新设计,甚至时间安排和预算限制驱动的。其次,应用程序的业务条件或市场变化可能会导致需要更改。第三,业务环境可能会增长或发生变化,这可能会改变项目优先级或客户或供应商工程团队的结构。需要记住的是,随着时间的推移,所有利益相关者都会更深入地了解系统;这种知识的增长是驱动大多数对软件请求/要求的修改的根本原因。因此,这是一个事实,许多软件工程师和项目经理难以接受,即大多数对软件的更改都是合理的!
配置管理处理软件工程项目的多分支功能。CM 是一组活动,这些活动已被开发出来以帮助管理在软件项目生命周期中发生的更改。在一个典型的项目中,可交付系统包含许多不同的文件和目录,并且它们之间可能会发展出许多复杂的关系。由于在开发过程中需要频繁修改,这个问题变得更加严重。CM 不善的结果包括
- 多次查找/修复同一个错误(版本之间不一致)
- 文档和代码之间不一致
- 文档丢失
- 代码丢失
在软件系统中,配置包括与系统相关的所有“工作产品”。这意味着不仅是实际交付给客户的系统,还包括开发人员维护的文档和支持代码。配置还可能包括针对不同平台的不同版本的可执行文件,并且可能具有与分配模块给编程团队成员相关的缺陷和任务。
为了理解与 CM 相关的問題,必须掌握作为典型软件项目一部分的庞大对象数量。即使是一个小型系统也可能包含超过一百个文件。对于规模合理的系统,这个数字可能会增长到数千甚至数万。与 CM 相关的問題可能与现代图书馆面临的挑战相似。如果没有仔细的库存控制,图书馆中的许多书籍就会被放错位置、放错架子、被盗,甚至丢失。
另一件需要记住的事情是,一个有效的 CM 计划是任何声称坚持能力成熟度模型® 集成 (CMMI) 原则的软件开发工作的一部分。这个系统由卡内基梅隆大学的软件工程研究所 (SEI) 创建,为软件开发提供了一种流程改进方法。它有 5 个级别,前三个级别都包含某种形式的已记录的 CM 流程。
Subversion (SVN) 在功能上似乎已经超过了 CVS(参见 TortoiseSVN)。但是,CVS 拥有一个相当不错的界面 WinCVS。
你可能听说过“过早优化是万恶之源”。这是真的。我建议你遵循以下准则。
- 始终先编写未优化的代码。
- 如果您的应用程序运行缓慢,请运行分析器以找出具体位置。
- 重写指示的代码以实现更优化的性能,但将旧代码保留在注释中。
- 重新测试,如果应用程序没有明显变快,则删除优化。
注意,这不是对无知的借口!作为一名程序员,您必须理解基本概念,例如,如果您正在执行插入排序,请选择链表而不是数组!这不是优化,而是编程!
请参阅 优化代码以提高速度,了解许多使程序运行更快的技巧。
我始终坚持的第一条规则是永远不要重复代码。即使只有一两行相似代码也会导致问题。永远不要复制粘贴。完全没有冗余的代码集称为“完全分解”。完全分解的代码比过度使用复制粘贴的代码更容易处理得多。
这也称为“DRY”原则:不要重复自己。
我甚至会说,您可以根据避免重复代码的难易程度来区分语言。
复制粘贴者通常会复制一个代码块并多次粘贴它,然后逐一更改几个常量以适应新代码块的需要。这些很容易识别,只需扫描代码中重复的行长模式即可。
解决方案通常是将常量提取到数组中,删除除一个之外的所有副本,然后遍历剩余的代码副本。
这通常会导致代码缩减的其他机会。例如,当代码中变化的“常量”是函数/方法调用(在每组重复代码的相同位置使用不同的方法名称)时,您可能会发现被调用方法中存在大量冗余。这也常常会导致创建新的对象和以前没有的可重用数据结构。好的优化通常会启动一系列重构,您可能会发现自己删除了如此多的代码以至于让人感到尴尬。
某些语言结构似乎只是吸引程序员进行代码复制。例如,设置 GUI 组件可能会非常重复。这是寻找代码缩减机会的好地方。Java 要求每个 MenuItem 对象都有一个类定义。您通常会看到它被实现为一屏代码,除了创建 MenuItem 对象之外什么也不做。通过定义一组数据并编写一个小型例程来创建所有 MenuItem 类,可以更容易地做到这一点(实际上更容易得多)。在这一点上,您会发现,像为每个菜单项添加按钮这样的操作,在重构之前可能很可怕,但现在变得微不足道了。
完全分解代码通常需要设置一些数据并遍历它。在 Java 中,在代码中设置一个数据数组非常简洁。稍后,数组数据可以非常容易地外部化,比最初的 C&P 代码更容易。要在 Java 中设置数组,请习惯使用这种简单的语法(其他语言应该有类似的结构)
String[] data = new String[] {"Opt1", "Opt2", "Opt3"};
字符串很常见,但您也可以使用 int 或 long 数组 - 语法相同。我经常使用 Object 数组将整数与字符串配对:new Object[] {"One", 1, "Two", 2}...(这将在 Java 5 中有效,在 Java 5 之前,您可能更适合使用两个数组。这些数组的技巧是保持语法简单而简洁。
在您的数据中通常也需要控制信号。不要害怕在代码中包含所需的信号。例如,如果您要自动生成一组菜单,您需要识别哪些菜单是顶级菜单,哪些是子项。这样的列表应该包含您需要的所有数据
"^", "File", "Load", "Save", "^", "Edit", "Copy", "Paste"
这些数据可以被输入一个简单的循环(最多几行)以创建您的整个菜单结构。
从重构中创建的这些数据结构通常最终会包含数据对或三元组——或者更糟糕的情况。只要您有想要组合在一起的数据对或集合,您就应该定义一个自定义对象来保存它们。我知道这听起来很极端,但试试看。您会突然发现自己一直都需要那个对象。您应该很快发现自己将曾经是静态实用程序方法(代码异味)的代码转移到您的新类中,并且它将具有完美的代码异味。
您可以创建一个字符串或对象数组并遍历它来创建您的自定义对象,但更好的方法是在数组中实际创建您的对象
以下是一个我在过去使用过的有点棘手的例子
MyClass[] primaryData=new MyClass[] { new MyClass("File", top), new MyClass("Save", "Save the file", "saveFunc"), new MyClass("Load", "Load the file", "loadFunc"), new MyClass("Edit", top), new MyClass("Copy", "Copy the selection", "copyFunc", "isTextSelectedFunc"), };
这使用了一些技巧。首先,有多个构造函数。最上面的是 (string, int),因此它与众不同,系统知道由此创建一个新的顶级菜单项。之后添加的所有 (没有 int “top”) 都将成为该菜单的成员。其余的构造函数接受字符串——一个字符串构造函数接受 3 个字符串,另一个接受 4 个。
第一个字符串显然是菜单名称。第二个是工具提示,第三个是调用函数的名称(在原始对象中)(这是通过反射完成的)。第四个,如果存在,则假定为返回 T/F 的布尔方法名称,可以根据返回值禁用该菜单项。
这段代码并不完全精确,它只是一个例子。要真正实现它,MyClass 需要访问一些其他东西(例如调用对象),但这可以给您一个想法。
使用这种结构来实现一个具有 20 或 30 个条目的典型菜单可以替换许多 C&P 代码屏幕。此外,您不必处理难看的“内部类”语法。您确实需要处理难看的反射语法,但这埋藏在 MyClass 对象中,只需要处理一次(永远)。
您可能还会注意到,现在所有字符串都在一个地方。这意味着用数据驱动的例程替换这几行代码将非常容易。这可能是下一步,也可能不是。我不建议尝试直接跳到数据驱动。
我以这种方式编码是为了说明将类存储在数组中的要点。如果您真的要这样做,我建议创建一个 MyClassHolder 实例并将调用类传递给它,以便 MyClassHolder 可以对其进行反射调用——然后,对于每个后续 MyClass 行,不要使用 “new”,而是调用 MyClassHolder 中的一个方法,该方法为您创建 MyClass 并将实例存储在自身内部。如果您想看到它真正实现,请在讨论标签中留言。此外,我可以根据它们只有一个参数的事实来区分“顶级”项,但我试图展示一种方法,如果您不能仅通过参数来区分每个方法,就可以使用这种方法。
请注意,上面的示例中数据存在一些冗余。
new MyClass("Save", "Save the file", "saveFunc"),
可以从 Save 计算 saveFunc。例如,您可以简单地说保存函数必须命名为 “doXFunc”。因此,创建一个“Save”选项会自动调用 doSaveFunc。
这在开始时通常有效,并且似乎是一个不错的技巧,但在我的经验中——我一直后悔将我的内部名称与外部显示的文本绑定,或者使用外部显示的文本作为任何形式的“键”。最终,有人会想用另一种语言或其他东西来实现它。我赞成简洁,但在这种情况下,它并不值得。
当你切换到数据驱动代码时,你生活中的大部分都会变得容易得多,但是某些类型的调试会变得困难得多——记住现在是你检查数据的工作。
- 明确定义数据,以便可以更改它。
- 对数据要极其挑剔,只允许你定义的结构和值。
- 检查数据中的引用(如果存在)。
- 以清晰的错误说明大声失败。
- 当用户忽略你的解释,并说这是你的代码的错误,而没有阅读它时,问问他们你可以做些什么来让他们阅读信息并解决他们的问题。把所有学到的东西放回代码中。
- 如果数据足够复杂,考虑实现一个编辑器。
- 数据可以很容易地存储在 .properties 文件(烦人)、XML 或数据库中——或者其他任何方式,选择最适合你的方式——.properties 文件使关联数据变得困难,而数据库几乎需要一个编辑器。XML 是一个很好的平衡。
确保验证数据与程序其余部分之间的界限。例如,在上面的例子中,在加载时检查每个反射字符串。事实上,你应该在加载 MyClass 对象时实例化 Method 对象,并存储方法对象,而不是每次都反射。
请记住,如果编写了像这样的错误检查代码,在数据中发现拼写错误将是微不足道的——否则几乎不可能。
匿名内部类是另一个导致 C&P 的情况。例如,每当控件的值发生变化时,你可能希望验证表单。最糟糕的情况:整个验证方法被 C&P 到每个匿名监听器中。更好的情况:每个监听器调用一个“验证方法”。
所有匿名监听器都应该相似,如果不是完全相同的话。
创建一个内部类(不是匿名的),它扩展你的基本监听器类型(ActionListener,...)。将一个匿名内部类中的代码放在这里。如果所有内部类都完全相同,你可以使该类无状态(没有内部变量)。
如果它是无状态的,创建一个实例,并将其传递到创建监听器的每个位置。你完成了。你可能将文件大小减少了 1/3 到 2/3,具体取决于其他正在进行的操作。你可能还在此过程中消除了 2 到 3 个与拼写错误相关的错误。
如果你的匿名内部类略有不同,你应该能够在实例化内部类时传入一个变量,并使用该变量来控制差异。如果你这样做,你将不得不创建多个类的实例——每个不同的类型一个。我仍然建议你使它们不可变,否则你需要为每个监听器创建一个实例。
如果它们有一些差异,你可以创建一个基类监听器并继承到一些子类中——就像你在任何 OO 代码中一样。没有理由仅仅因为你总是将它们视为匿名来对监听器进行不同的处理。
最后,如果它们非常不同,只需创建 2 或 3 个不同的类。你可以将任何唯一的匿名内部类保留下来,但是如果监听器中的代码是相同或非常相似的,请尝试将它们合并。
尝试将函数/方法限制在一个屏幕内。你会经常失败,但这是一个很好的目标。如果你看到一个函数长度达到 2 或 3 个屏幕,你应该开始感到很不舒服。
处理一堆小函数比处理一个大函数容易得多。
请不要通过串联来做。仅仅因为函数太长而将其打断在中间,甚至更糟。
正如我在注释部分所说,好的 OO 代码看起来什么也没做——任何地方。我认为我从未见过 Java 类中比一个屏幕更长的方法(删除注释后),绝大多数都是 1-3 行代码。我刚扫描了 Hashtable。没有一个方法超过一个屏幕(比如 25 行左右),大多数更小——所有都很小,专注于解决一个问题。
每个方法或函数都应该只做一件事。这种功能细分使管理更加容易,并且可以提供在计划过程中可能没有意识到的抽象级别。通常,如果代码运行超过 40 行,你可能在一个步骤中做得太多。如果你觉得该功能不应该公开给其他开发人员,请将该方法设置为私有。
我得出的结论是,每个变量都应该是私有的。总是。有时我会实现一个简单的类——矩形,甚至“Pair”,并将变量设为公有,这样我就不必编写 setter/getter。我最终总会后悔的。
即使是受保护的变量也很烦人。如果你必须这样做,编写一个简单的受保护的 getter(getter 和 setter 也可能是个坏主意——在谷歌上搜索“Getters and Setters are evil”以获取一篇很棒的文章)。
传递一个可变对象与全局变量一样糟糕。要非常小心可变对象(大多数 Java 对象,如 String,是不可变的,因此更安全)。
- 版本控制 wikibook 讨论了配置管理
- 计算机编程 wikibook 讨论了各种编程语言
- "提高开发人员的大学水平" 讨论了我们希望在学校学到的工具:版本控制、错误跟踪、夜间构建、沟通技巧、时间估计和管理、矛盾的现实。