跳转到内容

如何编写程序/收集真实经验

来自维基教科书,开放世界开放书籍

为什么是这本书

[编辑 | 编辑源代码]

良好的实践和生活经验从未在书籍中被记录下来。大多数计算机科学书籍都是 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 )

不要忽视代码模块中的文件头。这些文件头很重要,因为它们提供了对代码操作的概要,以及作者和许可信息。大多数编辑器程序在屏幕顶部打开代码模块,第一行代码位于最上面;在文件的最上面部分包含一个块注释,该注释指定该模块的功能,这在维护期间非常有用。

写注释并不是一项难以掌握的技能;事实上,很容易为几种语言创建一组模板,其中包含标准注释形式,然后在开始一个新模块时复制模板,添加代码和相应的注释细节。一些集成开发环境工具,比如EclipseX-Code,可以配置为在创建新源文件时自动执行此操作。

最后,在中大型项目中,使用自动文档工具,比如DoxygenJavaDocsAutodoc,为项目中的模块创建超文本链接文档,可能会很有帮助。这样就可以为项目的所有成员提供相同的标准文档,以便每个人都能了解项目中所有代码模块的可用性和操作细节。

要了解一些可用的基于代码的文档,可以查看库中的Java源代码。每个类都有一个注释块,显示一般类的用法,每个外部方法都有一个注释块,描述用法和注意事项,每个非平凡的方法都有内联(非javadoc)注释,描述每个代码块(通常是每行代码)正在进行的操作。总的来说,可能每行代码有5-10行注释,但方法注释比需要更详细,除非你是为公开使用而构建一个库。

(在阅读Sun的源代码时,你会发现大多数方法只有1-3行。好的面向对象设计看起来像“魔法”——你认为一定有一个巨大的例程存在于某个地方,做着所有的“实际工作”,但你永远看不到它,它只是这些小型的1-3行例程,一直到中心。)

如果你被键入注释(即使是每行代码3行注释)所拖累,那么你要么非常需要一个打字课,要么你打字速度快于你的思考速度!请花时间写你的代码——不要只是草率地写完,检查它,然后转到下一个。在你重构你的第一遍代码时,审查和重写注释。你可能认为你正在做你的老板想要的事情,因为你尽可能快地打出代码更改和错误修复,但实际上你正在做的是(根据我的经验)让你的整个团队因为严重的错误数量和可维护性问题而被解雇。

配置管理

[edit | edit source]

配置管理,也称为“CM”(有时也称为“SCM”,表示“软件CM”),是一个被误解的主题。虽然最近态度有所改变,但普遍的看法似乎是CM是一个“必要的邪恶”,应该容忍,但不应该积极参与,至少不要超过绝对必要。

当计算机软件被构建时,变化是不可避免的,而变化会增加在项目中工作的开发人员之间的混乱程度。当修改在执行之前没有被分析、在进行之前没有被记录下来、没有以一种提高质量、减少错误的方式进行控制,或者没有适当地向应该知道的人员报告时,就会出现混乱。

需要更改的原因有很多。首先,客户可能会提出一个或多个新的需求,或者可能会提出更改现有需求的请求。这可能是由设计评审、应用程序的重新工程,甚至是进度和预算限制驱动的。其次,应用程序的业务条件或市场变化可能会导致需要更改。第三,业务环境可能会增长或变化,这可能会改变项目的优先级或客户或供应商工程团队的结构。要记住的是,随着时间的推移,所有利益相关者都会对系统有更深入的了解;这种知识的增长是导致绝大多数软件修改请求/要求的原因。因此,这是一个事实,许多软件工程师和项目经理很难接受,即大多数软件更改都是合理的

配置管理处理软件工程项目的多版本功能。CM是一组被开发出来的活动,用于帮助管理软件项目生命周期中发生的更改。在一个典型的项目中,可交付的系统包含许多不同的文件和目录,它们之间可能会发展出许多复杂的关系。在开发过程中需要频繁修改,这个问题被进一步加剧。糟糕的CM的最终结果包括

  • 多次查找/修复同一个错误(版本之间不一致)
  • 文档和代码之间不一致
  • 文档丢失
  • 代码丢失

在一个软件系统中,配置包括与系统相关的所有“工作产品”。这不仅意味着实际交付给客户的系统,还包括开发人员维护的文档和支持代码。配置还可以包括针对不同平台的不同版本的可执行文件,并且可能包含相关的缺陷和任务,这些缺陷和任务管理着模块分配给编程团队的成员。

要了解与CM相关的问题,重要的是要了解作为典型软件项目一部分的大量对象。即使是一个小型系统也可能包含超过一百个文件。对于现实大小的系统,这个数字可能会增长到数千甚至数万。与CM相关的问题在范围上可能类似于现代图书馆所面临的问题。如果没有仔细的库存控制,图书馆中的许多书籍将会被放错地方、放错架子、被盗,甚至丢失。

另一件需要记住的事情是,有效的CM计划是任何声称坚持能力成熟度模型®集成(CMMI)原则的软件开发工作的一部分。这个系统是由卡内基梅隆大学软件工程研究所(SEI)创建的,它为软件开发提供了一种流程改进方法。它有5个级别,其中前三个级别都包含某种形式的已记录的CM流程。

CVS/Subversion

[edit | edit source]

Subversion (SVN) 似乎在功能上已经超越了CVS(参见 TortoiseSVN)。但是,CVS 有一个非常不错的界面 WinCVS。

优化

[edit | edit source]

你可能听说过“过早优化是万恶之源”。这是真的。我建议你遵循以下准则。

  • 首先总是编写未优化的代码。
  • 如果你的应用程序太慢,运行一个分析器来找出到底是哪里。
  • 重写指示的代码以使其更加优化,但将旧代码保留在注释中。
  • 重新测试,如果你的应用程序没有明显更快,就删除优化。

注意,这不是对无知的借口!作为一个程序员,你必须理解一些基本概念,比如如果你正在进行插入排序,选择一个链表而不是一个数组!这不是优化,而是编程!

有关使程序运行更快的多种方法,请参见优化代码以提高速度

避免代码重复

[edit | edit source]

我所有规则中最重要的规则就是永远不要重复代码。即使只有一两行类似的代码也可能导致问题。永远不要复制和粘贴。一套绝对没有冗余的代码称为“完全分解”。完全分解的代码比使用过多的复制和粘贴创建的代码容易处理得多。

这也称为“DRY”原则:不要重复自己。

我敢说,你可以通过语言避免重复代码的难易程度来区分它们。

识别重复代码

[edit | edit source]

复制粘贴者通常会复制一个代码块并粘贴多次,然后更改一些常量以适应新代码块的需求。这些很容易识别,只需扫描代码中重复的行长度模式即可。

从重复的代码块中提取唯一的数据

[edit | edit source]

通常的解决方案是将常量提取到数组中,删除除一个之外的所有重复代码,然后遍历剩余的代码副本。

这通常会导致代码减少的其他机会。例如,当代码中变化的“常量”是函数/方法调用(在每个重复代码集中,同一位置的不同方法名)时,您可能会发现被调用的方法存在很多冗余。这通常还会导致创建以前没有的新对象和可重用的数据结构。良好的优化通常会引发一系列重构,您可能会发现自己删除了如此多的代码,以至于令人尴尬。

某些语言结构似乎会吸引程序员进行代码复制。例如,设置 GUI 组件可能非常重复。这是一个寻找代码减少机会的好地方。Java 要求每个 MenuItem 对象都定义一个类。通常你会看到这种实现方式是用一屏代码来创建 MenuItem 对象,而实际上它可以用定义一组数据和编写一个小型例程来创建所有 MenuItem 类来更轻松地完成(实际上要容易得多)。此时你会发现,像为每个菜单项添加按钮这样的操作,在重构之前可能很可怕,现在变得微不足道。

完全分解代码通常需要设置一些数据并对其进行迭代。在 Java 中,在代码中设置一个数据数组非常简洁。之后,数组数据可以非常轻松地外部化,比原来的复制粘贴代码要容易得多。要在 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"

这些数据可以被输入到一个简单的循环中(最多几行)来创建您的整个菜单结构。

下一步,创建一个新类!

[edit | edit source]

从重构中创建的这些数据结构通常最终会包含数据对、三元组或更糟的情况。任何时候您都有想要分组在一起的数据对或集,您都应该定义一个自定义对象来保存它们。我知道这听起来很极端,但尝试一下。您会突然发现您一直都需要那个对象。您会很快发现自己将以前是静态实用方法(不好的代码气味)的代码移动到您的新类中,它将具有完美的代码气味。

您可以创建一个字符串或对象数组并对其进行迭代以创建自定义对象,但更好的方法是直接在数组中创建对象

这里有一个我在过去使用过的方法,有点棘手

  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 个条目的菜单可以替换许多屏幕的复制粘贴代码。此外,您不必处理难看的“内部类”语法。您确实需要处理难看的反射语法,但这被埋藏在 MyClass 对象中,只需要处理一次(永远)。

您可能还会注意到,现在您所有的字符串都在一个地方。这意味着用数据驱动的例程替换这 few 行代码会非常容易。这可能是下一步,也可能不是。我不建议尝试直接跳转到数据驱动。

关于代码示例的说明

[edit | edit source]

我以这种方式编写代码是为了说明将类存储在数组中的要点。如果你要真正这样做,我建议创建一个 MyClassHolder 实例并传入调用类,以便 MyClassHolder 可以对其进行反射调用 - 然后,对于每个后续的 MyClass 行,不要使用 "new",而是调用 MyClassHolder 中的一个方法,该方法为你创建 MyClass 并将实例存储在自己内部。如果你想看到真正的实现,请在讨论标签中留言。此外,我本可以使用只有一个参数的事实来区分“顶级”项目,但我试图展示一种在您无法仅通过参数来区分每个方法时可以使用的方法。

警告:不要依赖可见字符串

[edit | edit source]

注意,上面的示例中数据有一些冗余。

new MyClass("Save", "Save the file", "saveFunc"),

例如,可以从 Save 中计算出 saveFunc。例如,您可以简单地说保存函数必须命名为 "doXFunc"。因此,创建一个 "Save" 选项将自动调用 doSaveFunc。

这通常在一开始有效,并且似乎是一个很好的技巧,但根据我的经验 - 我总是后悔将我的内部名称绑定到外部显示的文本,或使用外部显示的文本作为任何类型的“密钥”。最终,有人会想用不同的语言实现它,或者其他什么。我赞成简洁,但在这个情况下,它不值得。

警告:小心使用数据

[edit | edit source]

每当你切换到数据驱动代码时,你生活中的大部分都会变得容易得多,但是某些类型的调试会变得困难得多 - 请记住,现在是你检查数据的工作。

  • 明确定义数据,以便可以更改它。
  • 对数据非常挑剔,只允许您定义的结构和值
  • 检查数据中的引用(如果存在)。
  • 用清晰的错误说明大声地失败。
  • 当用户忽略你的解释并说这是你的代码错误而没有阅读它时,问问他们你应该怎么做才能让他们阅读信息并修复他们的问题。把你学到的所有东西都放回代码中。
  • 如果数据足够复杂,请考虑实现一个编辑器。
  • 数据可以很容易地存储在 .properties 文件(烦人)、XML 或数据库中 - 或任何其他方式,选择对你最简单的方式 - .properties 文件使得关联数据变得困难,而数据库几乎需要一个编辑器。XML 是一个很好的平衡。

一定要验证数据与程序其余部分之间的界限。例如,在上面的示例中,在加载时检查每个反射字符串。事实上,你应该在加载 MyClass 对象时实例化 Method 对象,并存储 Method 对象,而不是每次都进行反射。

请记住,如果你编写了像这样的错误检查代码,那么数据中的拼写错误很容易检测到 - 否则几乎不可能检测到。

匿名内部类

[edit | edit source]

匿名内部类是导致复制粘贴的另一个情况。例如,每当控件的值发生更改时,您可能希望验证表单。最糟糕的情况:整个验证方法被复制粘贴到每个匿名监听器中。更好的情况:每个监听器都调用一个“验证方法”。

所有匿名监听器应该相似,如果不是完全相同。

创建一个内部类(非匿名),继承您的基本监听器类型(ActionListener 等)。将来自匿名内部类之一的代码放入这里。如果所有内部类都相同,您可以将该类设为无状态(没有内部变量)。

如果它是无状态的,创建一个单例并将其传递到每个创建监听器的位置。您就完成了。根据其他正在进行的操作,您可能将文件大小减少了 1/3 到 2/3。您可能还在此过程中消除了 2 到 3 个与打字错误相关的错误。

如果您的匿名内部类略有不同,您应该可以在实例化内部类时传递一个变量,并使用该变量来控制差异。如果您这样做,您将不得不创建多个类实例——每个不同类型一个。我仍然建议您将它们设为不可变的,否则您需要为每个监听器创建一个实例。

如果它们有一些差异,您可以创建一个基类监听器并继承到几个子类——就像您对任何面向对象的代码一样。没有理由仅仅因为您几乎总是将它们视为匿名类而对监听器进行不同的处理。

最后,如果它们非常不同,只需创建 2 或 3 个不同的类。您可以将任何唯一的匿名内部类保留下来,但是如果监听器中的代码相同或非常相似,请尝试将它们合并。

函数/方法大小

[编辑 | 编辑源代码]

尝试将函数/方法限制在一个屏幕内。您会经常失败,但这是一个很好的目标。如果看到一个函数长度超过 2 到 3 个屏幕,您应该开始感到很不舒服。

处理一堆小函数比处理一个大函数要容易得多。

请不要通过级联来实现这一点。仅仅因为函数太长而将其在中间断开,这更糟糕。

正如我在注释部分所说,好的面向对象代码看起来像是没有做任何事情——任何地方。我认为我从未见过 Java 类中的方法比一个屏幕更长(删除注释后),而且绝大多数只有 1-3 行代码。我刚刚扫描了 Hashtable。没有一个方法超过一个屏幕(比如 25 行左右),大多数更小——所有方法都非常小且专注于解决一个问题。

每个方法或函数都应该只做一件事。这种功能分解使管理变得更容易,并且可以提供在计划过程中可能没有意识到的抽象级别。一般来说,如果代码运行超过 40 行,您可能在一个步骤中做了太多的事情。如果您觉得该功能不应该公开给其他开发人员,请将该方法设为私有。

公共变量/变量范围

[编辑 | 编辑源代码]

我已经得出结论,每个变量都应该是私有的。始终如此。有时我会实现一个简单的类——矩形甚至“Pair”,并将变量设为公共的,这样我就不必编写 setter/getter。但我最终总是后悔这样做。

即使受保护的变量也很烦人。如果您必须这样做,请编写一个简单的受保护的 getter。(getter 和 setter 也可能是一个坏主意——在 Google 上搜索“Getter 和 Setter 很糟糕”以获取一篇很棒的文章。)

传递一个可变对象就像传递一个全局变量一样糟糕。对可变对象要非常小心(大多数 Java 对象像 String 都是不可变的,因此更安全)

进一步阅读

[编辑 | 编辑源代码]
  • 版本控制 维基教科书讨论了配置管理
  • 计算机编程 维基教科书讨论了各种编程语言
  • "改进开发者大学" 讨论了我们希望在学校学到的工具:版本控制、错误跟踪、夜间构建、沟通技巧、时间估计和管理、矛盾的现实。
华夏公益教科书