🏗️

软件工程

软件规模越大,越需要工程化的方法来保证质量、进度与成本可控。本章介绍软件危机与软件工程的由来、 软件生命周期、典型过程模型、需求分析、软件设计、编码、测试、维护以及软件项目管理与 UML。

📚 9 个小节🔄 生命周期模型🧪 黑盒白盒测试📋 项目管理

1软件工程概述

1.1 软件危机

20 世纪 60 年代,随着软件规模迅速增大,软件开发陷入严重困境:项目经常延期、预算超支、质量低劣、维护困难, 1968 年北大西洋公约组织(NATO)的学术会议上把这种现象称为"软件危机"。其典型表现:

  • 开发成本和进度估计不准,经费预算一再突破;
  • 用户对已完成的软件系统不满意(需求理解偏差);
  • 产品质量差、可靠性低,常常出错甚至崩溃;
  • 缺少文档,软件难以维护,维护成本占比越来越高(常占全部成本的一半以上)。

危机的根源:一方面软件本身是逻辑产品、复杂且易变、规模庞大;另一方面是开发方法不当——忽视需求分析、缺乏工程规范、过分依赖个人技巧。

1.2 软件工程的定义

软件工程(Software Engineering):采用工程的概念、原理、技术和方法来开发与维护软件, 把经过时间考验而证明正确的管理技术和当前最好的技术方法结合起来,经济地开发出高质量软件并有效维护。1968 年由 Fritz Bauer 在 NATO 会议上首次提出。

软件工程的三要素:方法(完成各项任务的技术方法,如需求分析方法、测试方法)、 工具(支持方法的自动化/半自动化工具,即 CASE 工具)、过程(为获得高质量软件所执行的一系列任务框架)。

1.3 软件质量特性(ISO/IEC 25010)

评价软件好坏不只看"能不能用",主要质量特性包括:功能性、可靠性、易用性、效率、可维护性、可移植性(经典 McCall/ISO 模型), 以及现代的安全性、兼容性等。好软件是在质量、成本、进度"铁三角"间取得平衡。

2软件生命周期

软件生命周期(SDLC):软件从提出开始,经过开发、使用和维护,直到退役报废的全过程。 通常划分为三个时期、八个阶段。
  1. 问题定义:明确"要解决的问题是什么",写出问题定义报告。
  2. 可行性研究:从技术、经济、操作、法律等角度判断项目是否值得做、能否做成。
  3. 需求分析:详细弄清楚目标系统"必须做什么",产出软件需求规格说明书(SRS)。
  4. 概要设计(总体设计):确定系统体系结构、划分模块、定义模块间接口。
  5. 详细设计:确定每个模块的具体算法和数据结构,回答"怎样具体实现"。
  6. 编码(实现):用程序设计语言把设计翻译成可执行的程序。
  7. 测试:通过各种测试发现并排除错误,包括单元测试、集成测试、系统测试、验收测试。
  8. 运行与维护:软件交付使用后持续排错、适应环境变化、扩充功能,直到退役。
📝

前三阶段属于软件定义时期(决定做什么、值不值得做),中间阶段属于软件开发时期,最后是运行维护时期。 一个重要规律:错误发现得越晚,修复代价呈指数级上升——需求阶段的疏漏若到测试期才发现,修复成本可能高达几十上百倍,所以前期分析设计至关重要。

3软件过程模型

软件过程模型(软件开发模型)是软件生命周期各项活动的组织框架,规定活动的顺序、交付物和过渡条件。常见模型:

3.1 瀑布模型

各阶段自上而下、像瀑布一样依次衔接:定义 → 可行性 → 需求 → 设计 → 编码 → 测试 → 维护,每个阶段有文档和评审,上一阶段确认后才进入下一阶段。

  • 优点:过程规范、文档齐全、便于管理,适合需求明确且稳定的项目。
  • 缺点:缺乏灵活性,用户要到很晚才能看到可运行软件,无法应对需求变更。

3.2 快速原型模型

先快速搭建一个可运行的原型给用户试用,根据反馈反复修改,逐步明确需求后再正式开发。优点是能有效减少需求误解;原型可能被丢弃或演进为最终系统。

3.3 增量模型与螺旋模型

  • 增量模型:把软件分成若干构件,分批设计实现交付,用户先用到核心功能,后续版本逐步增加功能。
  • 螺旋模型:将瀑布与原型结合,每一圈螺旋都包含制定计划 → 风险分析 → 实施工程 → 客户评估,特别强调风险分析,适合大型、高风险项目。

3.4 喷泉模型与敏捷开发

  • 喷泉模型:面向对象的开发模型,各阶段无明显边界、可反复迭代(如水花交叠),自下而上迭代、无缝。
  • 敏捷开发(Agile):应对需求快速变化的一系列轻量方法的总称,核心价值观强调"个体与互动、可工作的软件、客户合作、响应变化"。 典型实践 Scrum:把开发分成 2~4 周的冲刺(Sprint),通过产品待办列表(Product Backlog)、每日站会、评审会迭代交付增量;XP(极限编程)强调测试驱动、结对编程、持续集成。
模型最适合的场景关键特征
瀑布模型需求稳定、类似已有系统线性顺序、文档驱动
原型模型需求模糊、人机交互多快速原型、用户反馈
增量模型需尽早交付核心功能分批交付、逐步增强
螺旋模型大型复杂高风险项目风险驱动、迭代+原型
敏捷 Scrum需求多变的互联网产品短迭代、快速响应、拥抱变化

4可行性研究与需求分析

4.1 可行性研究

在项目正式立项前,用最小代价在尽可能短的时间内确定问题是否能够解决、是否值得解决,通常从四个方面论证:

  • 技术可行性:现有技术、人员、设备能否实现?
  • 经济可行性:成本—效益分析(开发成本 vs 预期收益、投资回收期)。
  • 操作(运行)可行性:系统在用户环境中能否被接受和顺利运行?
  • 法律与社会可行性:是否侵权、违反法规,对组织和社会有何影响?

产出可行性研究报告和粗略的项目开发计划,供决策者拍板。

4.2 需求分析

需求分析要准确回答"系统必须做什么",而不是"怎么做"。包括:

  • 功能需求:系统必须提供的功能(如登录、下单、查询成绩)。
  • 非功能需求:性能(响应时间)、安全性、可靠性、易用性、可扩展性等质量约束。
  • 通过访谈、问卷、跟班作业、用例分析等方式获取需求,经用户确认形成 软件需求规格说明书 SRS——这是开发方与用户之间的"合同",也是后续设计、测试和验收的依据。

4.3 结构化分析方法(SA)

面向过程的经典需求分析方法,思想是"自顶向下、逐层分解",主要工具:

  • 数据流图(DFD):用四种图形元素描述数据流动与加工—— 外部实体(方框)、加工/处理(圆或圆角矩形)、数据存储(双杠/开口矩形)、数据流(带箭头的线)。
  • 数据字典(DD):对 DFD 中所有数据流、数据项、数据存储、加工给出精确定义,是 DFD 的补充说明。
  • 加工逻辑可用判定表、判定树、结构化语言描述。

面向对象的需求分析则使用用例图描述参与者与系统功能的关系(UML 的一部分,见第 9 节)。

⚠️

需求阶段最忌开发人员"想当然"。需求必须完整、一致、无歧义、可验证,并由用户签字确认;含糊的需求(如"界面要好看")应转化为可度量的指标。

5软件设计

设计阶段把需求模型转化为可实现的解决方案,分概要设计与详细设计两步。

5.1 概要设计(总体设计)

  • 确定系统体系结构:如分层架构(表现层—业务层—数据层)、C/S、B/S、MVC 模式、微服务架构。
  • 模块划分与接口设计:把系统划分为若干模块,规定模块间调用和数据传递关系。
  • 设计数据结构、数据库(配合 E-R 模型)、出错处理和部署方案。

5.2 核心设计原则:高内聚、低耦合

内聚(模块内部)

一个模块内部各元素彼此结合的紧密程度。内聚越高越好:模块只做好一件事(功能内聚最佳),避免把毫不相干的功能塞在一起(偶然内聚最差)。

耦合(模块之间)

模块间相互依赖、连接的程度。耦合越低越好:接口简单清晰、少用全局变量,尽量使用数据耦合(只传必要参数),避免内容耦合(直接访问对方内部代码)。

面向对象设计还有著名的 SOLID 原则(单一职责、开闭原则、里氏替换、接口隔离、依赖倒置)以及"信息隐藏"思想。

5.3 详细设计工具

详细设计确定每个模块的算法与控制流程,但不写具体代码,常用描述工具:

  • 程序流程图:直观但容易随意跳转;N-S 图(盒图):用嵌套方框强制结构化;
  • PAD 图(问题分析图):二维树形结构,支持结构化;判定表/判定树:描述复杂条件组合;伪代码(PDL):类语言描述算法。

6软件编码

编码是把详细设计翻译为用程序设计语言书写的源程序的过程。编码质量直接影响可维护性,关键在于规范而非炫技。

6.1 语言选择与编码规范

  • 语言选择考虑应用领域、团队熟悉度、性能要求、生态与可维护性(系统底层选 C/C++,企业应用选 Java,AI 与脚本选 Python,前端选 JavaScript)。
  • 良好的编码风格:标识符见名知义;统一缩进与括号风格;关键逻辑写注释(解释"为什么"而非复述"做什么"); 函数短小、单一职责;避免深层嵌套和魔法数字;规范文件组织。

6.2 代码审查与版本管理

  • 代码审查(Code Review):同行阅读评审代码,是发现缺陷、统一风格、知识共享的有效手段,优于单纯依赖测试。
  • 版本控制:用 Git 等工具管理代码历史与分支,支持多人协作、回退和并行开发;配合 GitHub/GitLab 进行 Pull Request/Merge Request 流程。
  • 持续集成 CI:频繁地把代码合入主干并自动构建、跑测试,尽早发现集成问题。

7软件测试

软件测试:为了发现程序中的错误而执行程序的过程。好的测试用例是能发现至今尚未发现的错误的用例; 测试只能证明错误存在,不能证明错误不存在(Dijkstra 名言)。

7.1 测试的基本原则

  • 测试应尽早、不断地进行;程序员应避免测试自己的程序;
  • 妥善保存测试用例;注意错误的"群集现象"(错误常扎堆出现);
  • 严格按测试计划执行,既要有合法输入,也要有非法和边界输入。

7.2 黑盒测试与白盒测试

黑盒测试(功能测试)

把程序看成不能打开的黑盒子,不考虑内部结构,只根据需求规格检查输入输出是否正确。常用方法:

  • 等价类划分:把输入域分成若干等价类,每类取代表值
  • 边界值分析:重点测边界(如范围 1~100 测 0、1、100、101)
  • 因果图、错误推测法

白盒测试(结构测试)

把程序盒子打开,基于内部逻辑结构设计用例,追求对程序路径的覆盖。常用方法:

  • 逻辑覆盖:语句覆盖、判定(分支)覆盖、条件覆盖、判定/条件覆盖、路径覆盖(覆盖程度由弱到强)
  • 基本路径测试:根据程序图的环复杂度确定独立路径集合

7.3 测试的四个阶段

阶段对象依据 / 说明
单元测试单个模块/函数详细设计;多采用白盒,由开发者完成,用桩模块/驱动模块配合
集成测试组装后的模块群概要设计;检查接口,策略有一次性组装与渐增式(自顶向下/自底向上)
系统测试整个系统(软硬件环境)需求规格;黑盒为主,含功能、性能、压力、安全、兼容性测试
验收测试用户参与确认α 测试(开发者在场,内部环境)与 β 测试(用户在实际使用环境试用)

调试(Debug)与测试不同:测试是发现错误,调试是确定错误根源并改正,常用方法有蛮干法、回溯法、原因排除法(演绎、归纳、二分)。

8软件维护

软件交付用户使用之后进入运行维护阶段。维护是生命周期中持续时间最长、花费最大的阶段。按目的分为四类:

维护类型目的占比(经验值)
改正性维护改正在开发期未发现、运行后暴露的错误约 20%
适应性维护使软件适应新的软硬件环境、法规政策变化约 25%
完善性维护根据用户反馈增加新功能、改进性能与体验(占比最大)约 50%
预防性维护主动改进,为将来的可维护性和可靠性打基础约 5%

维护中要经过提出维护申请 → 评价与审批 → 修改程序及相关文档 → 回归测试 → 交付的受控流程, 不能拿到问题就直接改代码。可维护性主要取决于软件的可理解性、可测试性和可修改性——它们其实由前期的设计质量和文档质量决定。

💡

维护与开发一样必须"文档先行、同步更新文档"。只改代码不改文档,会让系统在下一次维护时变成无人能懂的"祖传代码"。

9软件项目管理与 UML

9.1 项目管理的核心内容

软件项目管理在人员、产品、过程、项目四个维度上展开,核心是"四控":进度控制、成本控制、质量控制、风险控制。

  • 成本估算:代码行估算法、功能点估算法、COCOMO 模型等;估算应基于工作量(人月)而非简单按工期线性加人——《人月神话》指出"向进度落后的项目增加人手只会使其更落后"(Brooks 定律)。
  • 进度管理:
    • Gantt 图(甘特图):用水平条形图表示各任务的起止时间,直观易懂,是最常用的进度工具,但不易表达任务间依赖。
    • PERT 图/工程网络:用箭头和节点表示任务及依赖,可计算关键路径——关键路径上的活动决定项目最短工期,其延期会导致整个项目延期。
  • 人员组织:主程序员组、民主制小组等;配置管理:管理软件的各种版本和变更(基线、版本库、变更控制委员会 CCB)。
  • 风险管理:识别、评估、监控风险(人员流失、需求变更、技术不成熟)并制定应对预案。

9.2 软件能力成熟度模型(CMM/CMMI)

用于评价软件组织的过程成熟度,从低到高分五级:初始级 → 可重复级 → 已定义级 → 已管理级 → 优化级。 等级越高,开发过程越规范、项目结果越可预期。

9.3 UML 统一建模语言

UML(Unified Modeling Language)是面向对象软件开发的标准可视化建模语言(不是开发方法),常用图包括:

图作用
用例图从用户角度描述系统功能与参与者(需求分析)
类图描述类、接口及其静态关系(关联、继承、依赖)
对象图、组件图、部署图对象实例、软件构件、系统物理部署结构
顺序图、协作图对象间动态消息交互的时序/合作关系
状态图、活动图对象状态转移、业务流程与并发行为
🔑

本章小结:软件工程用工程化方法应对软件危机;软件生命周期"定义—开发—维护"由不同过程模型(瀑布、原型、增量、螺旋、敏捷)组织; 需求分析用 DFD/用例把"做什么"说清楚,设计追求高内聚低耦合,编码讲究规范与评审,测试用黑盒/白盒在四个阶段层层把关,交付后在受控流程中持续维护,项目管理用甘特图、关键路径和成熟度模型保障进度成本质量。最后一章,让我们展望正在改变世界的计算机新技术。