软件创建

开发计算机软件是一项非常复杂的工作。在过去的十五年中,研究者们已经明确了这项工作的主要方面,包括以下几个关键阶段:

问题定义

  • 目的:明确软件需要解决的问题。
  • 方法:与利益相关者进行沟通,确定软件需求的初步想法。

需求分析

  • 目的:深入理解并定义软件的功能和约束条件。
  • 方法:收集详细的用户需求,编写需求文档。

实现计划

  • 目的:制定实现软件所需的详细步骤和时间表。
  • 方法:项目管理和资源分配。

总体设计

  • 目的:定义软件的架构和高层次结构。
  • 方法:创建设计文档,明确系统的组成部分和它们之间的交互。

详细设计

  • 目的:具体化每个组件的设计。
  • 方法:详细说明具体模块的功能和接口。

创建即实现

  • 目的:将设计转化为实际代码。
  • 方法:编程和代码实现。

系统集成

  • 目的:将不同模块或组件整合为一个完整的系统。
  • 方法:集成测试,确保各部分协同工作。

单元测试

  • 目的:验证单个模块或组件的功能。
  • 方法:编写和执行针对单一模块的测试用例。

系统测试

  • 目的:确保整个系统满足设计和需求规格。
  • 方法:进行全面的测试,包括功能测试、性能测试等。

校正性的维护

  • 目的:修复软件运行中出现现的错误和问题。

  • 方法:定期更新软件,解决已发现的缺陷和性能问题。

功能强化

  • 目的:根据用户反馈和市场变化,增加新的特性或改进现有功能。
  • 方法:持续迭代开发,增强软件的功能和性能。

隐喻编程

隐喻在软件开发中起着关键的启发性作用,它通过将软件开发过程与我们熟悉的事物相比较,帮助我们更深入地理解软件开发的本质。以下是几个重要的隐喻及其在软件开发中的应用。

计算机科学的语言隐喻

  • 描述:计算机科学的语言丰富多彩,包括病毒、蠕虫、臭虫、炸弹等。
  • 意义:这些形象的隐喻描述了特定的软件现象,有助于我们理解和描述软件中的复杂问题。

播种与培植庄稼的隐喻

  • 描述:将创建软件比作播种或培植庄稼。
  • 意义:强调通过小步前进的方法(增量设计、构造、测试)来减少错误,提高软件质量。

沉积与增量的隐喻

  • 描述:将软件开发比作从水中沉积物中积累出代码。
  • 意义:强调在系统中逐渐添加新功能的重要性,即增量设计和测试。

建造房屋的隐喻

  • 描述:将软件创建比作建造房屋的过程。
  • 意义:展示了软件开发的各个阶段,从问题定义到优化,与建房子的步骤相类似。

利用现成资源的隐喻

  • 描述:在建房时,使用现成的洗衣机、电冰箱等,而不是自己制造。
  • 意义:鼓励在软件开发中利用现有的资源,如库、框架和高级语言特性。

定制化建造的隐喻

  • 描述:如果要建造一流的别墅,可能会选择定制家具和装饰。
  • 意义:对于高级别的软件开发,可能需要创建定制化的组件和功能,以实现特定的需求。

智能工具箱的隐喻

  • 描述:软件开发实践像一个装满各种工具的工具箱。
  • 意义:强调没有万能的工具,为每个任务选择合适的工具是成为优秀程序员的关键。

结论

  • 隐喻的重要性:隐喻帮助我们通过比较熟悉的事物来理解软件开发的复杂性。
  • 选择合适的隐喻:不同的隐喻适用于不同的情境。选择恰当的隐喻可以帮助更好地解决问题和理解软件开发的特性。
  • 工具箱理念:每位程序员都有自己的技术和方法集合。在不同的开发阶段选择合适的技术和方法是至关重要的。

软件创建的先决条件

先决条件的重要性

软件开发过程中的准备工作对于最终产品的质量至关重要。以下是几个关键点:

  1. 项目启动前的准备:像建筑工人在修建房屋前做准备一样,优秀的程序员在项目开始前会进行充分的准备。这包括确保所有必要的资源已准备就绪,并在计划的每个阶段都注重高质量。
  2. 抵制立即编码的冲动:许多程序员倾向于立即开始编码,但应先考虑准备阶段可能出现的问题。准备工作的经验是必不可少的,特别是在大型项目中。
  3. 教育周围的人:程序员的职责之一是教育管理人员和同事理解技术项目的开发过程,确保他们理解先决条件的重要性。
  4. 项目规划的重要性:在大型项目开始前进行彻底的规划至关重要。这与建造房屋前的规划工作相似。
  5. 避免不必要的修改:研究表明,最好是一次完成项目,因为不必要的修改成本很高。错误应尽早在软件开发过程中被发现并修正。

问题定义先决条件

在需求分析之前,应该清晰地定义问题。良好的问题定义应遵循以下准则:

  • 专注于问题本身:问题定义应仅描述要解决的问题,而不涉及解决方案。例如,“我们无法跟上指令系统”是一个问题,而“我们需要优化数据入口系统以便跟上指令系统”则偏向解决方案。
  • 从用户视角出发:问题定义应使用用户的语言,避免使用计算机技术术语。这有助于确保最佳的解决方案可能并不局限于技术实现。

需求分析的先决条件

需求分析是确定系统功能的关键步骤,应注意以下要点:

  1. 明确需求的重要性:清晰的需求有助于由用户而非程序员决定系统功能。这可以防止程序员在编程过程中对用户需求进行猜测。
  2. 需求分析的质量:如果需求分析不佳,应停止当前工作并返回需求分析阶段。
  3. 管理变更请求:对于在需求分析阶段之外提出的新想法,应进行成本和进度估计,然后再决定是否采纳。
  4. 应对不稳定需求:如果需求特别奇怪或频繁变化,考虑放弃项目可能是一个明智的选择。

结构设计先决条件

结构设计阶段需要考虑以下关键因素:

  1. 模块功能的明确性:每个模块应只完成一项任务,并应尽可能降低模块间的相互了解程度。
  2. 变动应对策略:结构设计应清晰描述系统应对变动的策略,包括对旧软件的重用和代码购买。
  3. 关键数据结构:明确使用的主要文件、表和数据结构,及其替代方案。
  4. 依赖特定算法的设计:如果设计依赖于特定算法,应详细描述或指出该算法。
  5. 内存管理:预估正常和极端情况下所需的内存。
  6. 错误处理:错误处理是软件开发中的关键部分,应在设计中明确处理策略。
  7. 坚固性和容错性:设计应指明所期望的系统坚固性和容错性类型。
  8. 性能目标:如果考虑性能,应在设计中明确性能目标,包括速度和内存使用。
  9. 设计一致性:确保结构设计的每次变动都与总体设计概念一致。

选择编程语言先决条件

选择编程语言时,应考虑以下因素:

  • 语言对思维的影响:编程语言会影响程序员的思维方式。确保所选语言适合项目的需求和团队的技术背景。

编程约定

在开始编码之前,明确的编程约定是至关重要的。这些约定应该详细到在编程过程中不需改动,确保代码的一致性和可维护性。

  • 编程风格:包括命名规范、代码布局、注释风格等。
  • 代码标准:定义函数、类的结构,变量的使用规则等。
  • 错误处理:统一的错误报告和处理机制。
  • 性能要求:对于性能的基本要求和目标。
  • 安全性:安全编程准则,防止常见的安全漏洞。

应花在先决条件的时间

软件项目的先决条件阶段需要投入适当的时间和资源,一般建议分配项目总时间的20%至30%。

  • 项目规模与复杂度:大型或复杂项目可能需要更多的时间进行准备。
  • 团队经验:经验丰富的团队可能在某些阶段更快地完成准备工作。
  • 风险评估:高风险项目需要更细致的先决条件分析。

编码前的技术评审

在正式开始编码之前进行技术评审,以确保设计的合理性和可行性。

  • 设计评审:评估设计方案是否符合需求。
  • 代码审查:早期发现潜在的代码问题和不一致之处。
  • 性能分析:确保设计能满足性能要求。

团队协作准备

确保团队成员间的有效沟通和协作。

  • 角色分配:明确每个团队成员的职责和任务。
  • 沟通机制:建立有效的沟通渠道和会议安排。
  • 文档共享:确保所有相关文档易于团队成员访问。

技术栈和工具选择

选择合适的技术栈和工具是项目成功的关键。

  • 编程语言:根据项目需求和团队技能选择合适的编程语言。
  • 开发框架:选择支持项目需求的开发框架和库。
  • 开发和测试工具:选用高效的IDE、版本控制系统和测试工具。

建立子程序的步骤

建立子程序是软件开发中的一个重要过程,需要遵循一系列明确的步骤以确保代码的质量和可维护性。

描述子程序的操作

  • 使用自然语言:以清晰的自然语言描述子程序的每一步操作,确保其目的和功能明确。

命名子程序

  • 清晰且具体:子程序的命名应清楚、具体,避免引起误解。
  • 反映功能:名称应反映子程序的功能和作用。

考虑效率

  • 性能优化:在保证功能和清晰性的基础上,考虑子程序的运行效率。
  • 资源管理:注意资源使用和管理,避免不必要的资源浪费。

算法和数据结构

  • 合适的选择:根据子程序的功能选择合适的算法和数据结构。
  • 效率与可维护性:平衡算法的效率和代码的可维护性。

从抽象到具体

  • 注释先行:编写子程序时,先从抽象的注释开始,明确子程序的目的和功能。
  • 清晰的目标:如果在这一步遇到困难,可能需要对子程序的角色和功能进行深入思考。

逐步细化数据

  • 数据设计:仔细考虑子程序将处理的数据类型和结构。

  • 逐步实现:从大致框架开始,逐步细化实现细节。

使用程序设计语言 (PDL)

  • 易懂的语言:使用清晰易懂的自然语言编写PDL,避免过度依赖特定编程语言的语法。
  • 注重目的:PDL应关注于描述子程序要做什么,而非具体实现方式。
  • 从PDL到代码:PDL可以直接转化为代码的注释,确保注释的准确性和实用性。

持续检查和改进

  • 代码检查:在开发过程的每一步中检查子程序,确保质量。
  • 同事审查:鼓励团队成员互相审查代码,及早发现并纠正错误。
  • 降低成本:通过早期发现和修正错误,降低后期修复成本。

高质量子程序的特点

生成子程序的原因

创建子程序的理由总结

  • 降低复杂性:使代码更易于理解和维护。
  • 避免重复代码:促进代码重用,减少冗余。
  • 限制改动影响:简化维护和更新。
  • 隐含顺序和控制:明确代码的执行流程。
  • 改进性能:通过优化关键子程序来提升整体性能。
  • 进行集中控制:统一管理相关操作。
  • 隐含数据结构和指针操作:隐藏复杂的数据处理。
  • 隐含全局变量:减少全局变量的直接使用。
  • 促进代码重用:提高代码的可重用性。
  • 计划开发软件族:为未来的扩展做准备。
  • 改善可读性和可移植性:使代码更易于阅读和适应不同环境。
  • 分隔复杂操作:将复杂的操作拆解成更小的单元。
  • 独立非标准语言函数的使用:减少对特定语言特性的依赖。
  • 简化复杂的布尔测试:使逻辑判断更清晰。

子程序命名

  • 清晰描述:名称应明确反映子程序的功能。
  • 适当的长度:长度应足以描述功能,但不过于冗长。
  • 避免模糊用语:选择明确无歧义的词汇。

强内聚性

内聚性类型

  • 功能内聚性:子程序执行单一且明确的功能。
  • 顺序内聚性:操作需要按特定顺序执行。
  • 通讯内聚性:操作使用相同的数据。
  • 临时内聚性:操作基于相同的时间发生。

松耦合性

耦合性的类型

  • 简单数据耦合:优选,通过参数表传递非结构化数据。
  • 数据结构耦合:通过参数表传递结构化数据。
  • 控制耦合:一个子程序通过参数控制另一个子程序的行为。
  • 全局数据耦合:子程序通过全局变量进行通信。
  • 不合理耦合:直接修改另一个子程序的内部数据或代码(应避免)。

子程序长度

  • 理想长度:一般建议子程序长度为一到两页代码(大约66到132行)。

防错性编程

  • 使用断言:验证程序状态,确保数据的正确性。
  • 垃圾输入处理:确保对不合理的输入给予合适的响应。
  • 异常处理:设计异常处理机制,应对意外情况。
  • 预计可能的改动:考虑未来可能的变化和扩展。
  • 去除调试代码:发布时移除专用于调试的代码部分。
  • 保留错误提示信息:对于错误和异常提供清晰的反馈。
  • 检查函数返回值:验证其他函数的返回值,确保数据的正确性。

子程序参数

参数设计和使用

  • 匹配实际与形式参数:确保调用时传递的参数与子程序定义匹配。
  • 参数顺序:输入、修改、输出参数的顺序应一致。
  • 类似子程序的参数顺序一致性:相似功能的子程序应有相同的参数顺序。
  • 使用所有参数:不使用的参数应从子程序接口中移除。
  • 状态和错误变量放在最后:使子程序的主要功能更明显。
  • 参数非工作变量:避免将参数用作内部的工作变量。
  • 特殊情况的说明:明确参数的特殊用途和限制。
  • 限制参数个数:减少参数数量,避免过度复杂。
  • 规范参数命名:清晰、一致的命名提高代码可读性。
  • 传递必要部分的结构化变量:仅传递子程序所需的数据结构部分。

模块化设计

模块的内聚性

  • 原则:模块应提供一组相互联系的服务,保持高内聚。
  • 意义:内聚性强的模块能更清晰地定义功能,简化理解和维护。

模块的耦合性

  • 原则:设计模块时应保持与其他模块的耦合度低。
  • 实践:模块应被设计成可提供完整功能,确保与其他部分的清晰交互。

封装和信息隐蔽

  • 定义:将模块的信息细节隐蔽起来,也称为“封装”。
  • 优点:
    • 便于修改。
    • 澄清代码编写意图。
    • 提高可靠性。
    • 简化调试。
    • 统一数据存取模式。

高级结构设计

设计流程

  1. 子系统划分。
  2. 模块化设计。
  3. 程序的细节设计。
  4. 子程序内部和设计。

设计方法

  • 自顶向下分解。
  • 自底向上合成。

设计原则

  • 抽象:创建与问题同等抽象层次的编程抽象,避免过度细节化。
  • 封装:在老旧语言中是自愿的,而在如Ada等现代语言中则是强制性的。
  • 继承:在面向对象编程中,继承性简化了编程,通过通用和专用子程序处理对象间的共同与不同特性。

面向对象设计步骤

  1. 识别对象及其属性。
  2. 确定对象的功能。
  3. 确定对象间的相互作用。
  4. 确定对象的可见性和接口。

往返设计

  • 概念:设计是一个迭代过程,可能需要多次往返以深化理解。
  • 好处:在设计过程中进行迭代可以带来更大的好处。

问题解决方法

  1. 理解问题。
  2. 设计方案。
  3. 执行解决方案并检查每一步。
  4. 回顾并检查答案。

数据名称

变量命名

  • 原则:变量名称应准确描述其代表的实体。
  • 建议:使用自然语言描述变量代表的实体,名称应体现问题而非解决方法。

名称长度

  • 标准:选择合适长度的变量名,确保清晰易懂。
  • 例外:在特定情况下,如临时变量,短名称(如i)可能更合适。

命名风格

  • 限定词使用:如totals, averages, maximums等,限定词放在变量名末尾。
  • 反义词使用:恰当使用反义词以增强变量意义的清晰度。

特殊类型的变量命名

  • 状态变量:避免使用模糊的名字如flag,应使用更具描述性的名称。
  • 临时变量:通常用作保存中间计算结果,应避免无意义的命名。

逻辑和枚举变量

  • 逻辑变量:使用肯定的名称,易于理解。
  • 枚举变量:使用统一的前缀或后缀以表明同一类型。

变量

作用域和初始化

  • 作用域:变量在程序中的可见范围。
  • 初始化:应在使用前恰当地初始化变量。

持久性和全局变量

  • 持久性:变量的使用寿命,避免硬编码,确保功能单一性。
  • 全局变量:最初设为局部变量,必要时转为全局变量,但应优先考虑使用存取子程序。

控制

循环和条件控制

  • 边界错误:明确循环结束点和条件语句中的等号情况,避免边界错误。
  • 简化循环:通过减少循环次数、明确进出口等方式简化循环。

嵌套和子程序

  • 简化嵌套:重新编写测试条件,将深层嵌套代码转为子程序或使用case语句替代if嵌套。

文档

文档类型

  • 综合资料:供开发者使用的非正式记录。
  • 详细设计文档:描述模块层或程序层的决策。

代码注释

  • 注释类别:包括代码的重复、解释、标记、总结和意图描述。
  • 注释原则:避免无关注释,注释应清晰表达代码意图。

注释实践

  • 行注释:避免过度使用,注释应与代码紧密相关。
  • 结束行注释:应集中在“为什么”而非“如何”上。

项目大小的影响

交流和方法

  • 交流规范化:对大型项目而言,规范化文档和交流方式非常重要。
  • 方法应用:小型项目可能采用偶然和本能的方法,而大型项目则需要精确和计划性的方法。

项目规模影响

  • 代码行数和参与人数:项目大小不仅由代码行数和参与人数决定,还包括软件的质量和复杂性。
  • 生产效率:受人员素质、编程语言、方法、产品复杂性等多种因素影响。

单元测试

测试原则

  • 测试覆盖:选择能有效发现错误的测试用例,包括结构测试和数据流测试。
  • 边界条件:重点关注边界条件和最小最大值。