第一章 软件的概念
软件工程,是研究如何用系统化、规范化、可度量的方法去开发、运行和维护软件的学科。本章从"软件是什么"这一最朴素的问题出发,依次展开软件的特性、分类、软件危机的来龙去脉,以及软件工程的基本方法学,为后续各章奠定概念基础。
1.1 软件的定义
软件(Software)是计算机系统中与硬件相互依存的另一部分,它是程序、数据以及相关文档的完整集合。这三者缺一不可:
- 程序(Program):按照预先设计的功能与性能要求,用某种程序设计语言编写的、能够在计算机上执行的指令序列。
- 数据(Data):使程序得以正常处理信息所必需的数据结构,以及其中包含的具体数据。离开了数据,程序便无从运行。
- 文档(Document):与软件开发、维护和使用相关的文字材料,例如需求规格说明、设计说明书、用户手册、测试报告等。文档是开发者、管理者与用户之间的沟通桥梁。
这里必须强调一点:软件不等于程序。只交付一段能跑的代码,而缺少配套的数据与文档,不能算作"完整的软件"。这一区分看似简单,却是理解后面"软件危机"与"软件工程"两节的关键——许多危机,恰恰源于人们把软件误当成了程序。
1.2 软件的特性
与硬件相比,软件在形态、生产方式、质量形成等方面都有本质不同。可以从以下十个方面来理解软件的特性:
① 形态特性
软件是一种逻辑实体,而非物理实体。它无形、无重、看不见也摸不着,必须依附于存储介质或运行环境才能被感知。这一特性使得软件难以像硬件那样被直接度量和检验。
② 智能特性
软件是人类智力活动的产物,是知识、算法与解决问题思路的载体。同一台硬件之所以能完成千差万别的工作,靠的正是其中运行的软件所蕴含的"智能"。
③ 开发特性
硬件是"制造"出来的,软件却是"开发(设计)"出来的。软件不会像工业产品那样经历批量生产的物理过程,每一行代码都需要人的创造性劳动。
④ 质量特性
软件的质量是在开发过程中逐步构建出来的,而不是在成品阶段"检验"出来的。代码一旦写就,缺陷便已埋下,因此质量保障必须贯穿需求、设计、编码、测试的全过程。
⑤ 生产特性
软件"产品"的获得极其廉价——核心成本集中在前期开发,一旦开发完成,复制(拷贝、部署)的边际成本几乎为零。这与硬件"每生产一件都要消耗物料"形成鲜明对比。
⑥ 管理特性
随着规模增大,软件开发早已不是单打独斗,而是多人协作的工程活动。进度、成本、人员、风险都需要系统化的项目管理,否则极易失控。
⑦ 环境特性
软件无法独立运行,必须依赖特定的硬件平台、操作系统、网络与中间件环境。环境的差异直接决定软件的可用性与可移植性。
⑧ 维护特性
软件交付后需要持续的维护(纠错、适应、完善),且维护成本往往占据软件生命周期总成本的绝大部分。与硬件"用久会磨损"不同,软件本身不会磨损,却会因频繁修改而逐渐"退化"。
⑨ 废弃特性
硬件会因物理损耗而被淘汰,软件却往往因运行环境变化或需求演变而过时——它不是"用坏"的,而是"不再适用"的。这决定了软件具有相对较短的有效生命周期。
⑩ 应用特性
软件的应用领域极为广泛,从航天控制到日常办公,几乎没有行业能脱离软件。不同领域的软件在规模、实时性、可靠性等要求上差异巨大。
1.3 软件的分类
按照功能与用途,软件大致可分为以下几类:
类别 | 说明 | 典型例子 |
① 系统软件 | 管理计算机硬件与基础资源,为其他软件提供运行平台 | 操作系统、编译程序、设备驱动程序 |
② 支撑软件(工具软件) | 辅助软件开发、测试、维护的工具与环境 | 集成开发环境(IDE)、数据库管理系统、调试器、版本控制工具 |
③ 应用软件 | 面向具体业务需求,直接为用户解决特定问题 | 文字处理、浏览器、企业管理(ERP)系统、游戏 |
④ 可复用软件 | 可供多个程序重复调用的标准化构件 | 各种标准函数库(如数学库、标准模板库 STL) |
1.4 软件危机
软件危机(Software Crisis)是指计算机软件在开发和维护过程中所遇到的一系列严重问题。它最直接的表现,就是软件的发展速度远远滞后于硬件的发展速度。具体症状包括:
- 软件开发周期长,远不能满足日益增长的需求;
- 开发成本高,且经常严重超出预算;
- 软件质量差,频繁出现错误与故障;
- 维护困难,修改一处往往引发新的问题。
造成软件危机的根本原因在于:计算机能力的飞速提升使软件的规模与复杂度急剧膨胀,而当时的开发方式仍停留在"个人手艺"层面,缺乏系统化、工程化的方法与工具。硬件遵循摩尔定律高速迭代,软件却跟不上脚步,供需之间的鸿沟由此形成。
软件危机的提出,正是催生"软件工程"这一学科的直接动因。
1.5 软件工程的基本方法
什么是软件开发
软件开发(Software Development)是指开发、运行、维护和修复软件的系统化方法。它强调用"工程化"的思路,而非零散的技巧,去对待软件的整个生命过程。
软件工程方法
实践中主要有两类方法:
- 传统方法(结构化方法):以功能分解和数据流为核心,自顶向下、逐步求精;
- 面向对象方法(OO 方法):以对象、类、继承、消息通信为核心,更贴近人类认知事物的方式。
软件工程方法学的三要素
任何一门软件工程方法学,都离不开三个基本要素:
- 方法(Method):完成软件开发各项任务的技术路线,回答"怎么做";
- 工具(Tool):为方法提供自动化或半自动化支持的软件与环境,用以提高效率与质量;
- 过程(Process):将方法与工具有机结合、规定任务顺序与里程碑的框架,回答"按什么步骤做"。
软件工具(CASE)的分类
计算机辅助软件工程(CASE, Computer-Aided Software Engineering)工具,可按所支持的活动划分为:
①支持软件开发过程的工具:如需求分析工具、设计工具、编码与调试工具; ②支持软件维护过程的工具:如版本管理、逆向工程、再工程工具; ③支持软件管理过程与支撑过程的工具:如项目管理、配置管理、质量管理工具。
本章小结
软件是程序、数据与文档的统一体,具有区别于硬件的十大特性。软件危机揭示了"凭经验开发"的困境,而软件工程则以方法、工具、过程三要素为支柱,借助 CASE 工具,将软件开发引向系统化、工程化的道路。理解了这些基本概念,我们才能在后续章节中从容地讨论生存期模型、需求分析、设计与测试。
第二章 软件生存期模型
上一章我们界定了"软件是什么"。这一章要解决的是"软件怎么造"——也就是软件从设想到退役,要经历哪些阶段、以什么顺序组织。把这套阶段与顺序固定下来的框架,就叫作软件生存期模型(也称过程模型)。
2.1 软件生存期与模型的概念
**软件生存期(Software Life Cycle)**指软件从提出开发设想开始,历经需求、设计、编码、测试、运行,直到最终被淘汰退役的全过程。为了驾驭这一漫长过程,人们把它划分为若干相对独立的阶段,每一阶段都有明确的任务、交付物与里程碑。
软件生存期模型则是对这些阶段如何组织、衔接与迭代的描述。选对模型,项目就成功了一半;选错模型,往往从一开始就埋下失控的种子。下面逐一介绍几种经典模型。
2.2 瀑布模型
瀑布模型是最早、也最直观的模型,其核心特征是顺序性与依赖性:
- 各阶段自上而下排列,如同瀑布逐级下落;
- 前一阶段完成后,才能开始下一阶段;
- 适用于项目开始时需求已确定、且较少变更的场景。
实际工程中常用的还有两种变体:
- 实际的瀑布模型:在相邻阶段之间加入反馈环,发现问题时可回退修正;
- V 模型:将测试与开发阶段对应起来(如单元测试对应详细设计、系统测试对应需求),强调"验证"贯穿始终。
优点是思路清晰、文档完备;缺点是过于刚性,需求一旦变更代价高昂,且要到后期才能看到可运行软件。
2.3 快速原型模型
如果说瀑布模型假设"需求一开始就知道",那么快速原型模型恰恰承认需求往往是模糊的。原型(Prototype)的本质用途,就是获知用户的真正需求:
- 快速构造一个可运行的简化版本(原型);
- 让用户实际操作、提出意见;
- 根据反馈反复修正;
- 需求真正摸清后,再正式开发产品。
它特别适合需求不明确、用户自己也说不清要什么的项目。
2.4 增量模型
增量模型把软件产品分解为一系列增量构件(Increment),逐个交付:先推出核心功能让用户体验,再分批加入后续构件。
它的好处是:早期就能拿到可用版本、用户反馈更及时、风险被分散到多个增量中。代价是要求软件架构具备良好的可扩展性,否则后续增量难以"拼"上去。
2.5 螺旋模型
螺旋模型将瀑布模型与快速原型模型结合,并加入了关键的风险分析。它以螺旋上的若干圈代表一轮轮迭代,每一圈都包含四个活动:
- 制定计划(确定目标与方案);
- 风险分析(评估方案、识别并化解风险);
- 实施工程(开发本轮的可交付物);
- 客户评估(用户评审,进入下一圈)。
它专为规模大、风险高的项目设计——每一圈都把"风险"摆在桌面上,而不是等到失败才发现。
2.6 喷泉模型
喷泉模型是与面向对象方法相关的模型。它的特点是各阶段相互迭代、无明显边界:分析、设计、编码等活动可以交错、反复进行,像喷泉的水珠上下翻涌。
这正契合面向对象开发中"分析即设计、设计即编码"的渐进特质,强调开发过程的无缝与可回溯。
2.7 统一过程 RUP
RUP(Rational Unified Process,统一过程)是一种以用例驱动、架构为中心的迭代过程,分为四个阶段:
阶段 | 目标 |
初始阶段 | 确定项目可行性、范围和商业 case |
细化阶段 | 建立稳定的架构基线,化解主要风险 |
构造阶段 | 完成产品的开发与测试 |
移交阶段 | 部署上线,交付用户使用 |
RUP 定义了若干核心工作流,贯穿所有阶段:业务建模、需求、分析与设计、实现、测试、部署。每一阶段都是这些工作流的一次迭代。
2.8 敏捷过程
进入 21 世纪,**敏捷(Agile)**方法因能应对快速变化的需求而兴起。其价值观集中体现为《敏捷宣言》的四条对比:
- 个体和交互胜过 过程和工具;
- 可工作软件胜过 宽泛的文档;
- 客户合作胜过 合同谈判;
- 响应变更胜过 遵循计划。
敏捷并非"不要过程",而是把"人、可用软件、客户、变化"放在更高优先级。
2.9 极限编程 XP
**极限编程(Extreme Programming, XP)**是敏捷方法中实践最具体、约束最鲜明的一种。它将一系列优秀实践"做到极致",典型包括:
- 测试驱动开发(TDD):先写测试,再写代码;
- 结对编程:两人一机,实时互审;
- 持续集成:频繁合并、随时可运行;
- 小规模发布与短迭代:快速交付、快速反馈;
- 重构:持续改善代码结构而不改行为;
- 简单设计、代码集体所有、现场客户等。
本章小结
从刚性的瀑布,到弹性的原型、增量、螺旋、喷泉,再到工程化的 RUP 与以人为本的敏捷/XP,生存期模型的演进,本质是一条主线:如何让软件开发更好地应对"需求不确定"与"风险不可控"。理解了这些模型的取舍,我们才能在第三章起,深入每个阶段内部究竟做什么。
第三章 软件需求分析
无论采用哪种生存期模型,"需求"都是第一个真正落地的关口。需求做错,后面全错——这正是需求分析被称为"最关键阶段"的原因。
3.1 需求分析的阶段
需求分析本身也是一组有序活动,通常分为四个子阶段:
- 需求获取:通过与用户交流、观察、调研,收集原始需求;
- 需求分析:梳理、提炼,分清哪些是实现约束、哪些是真正的功能需求;
- 需求定义:将分析结果用规范语言写清楚;
- 需求验证:与用户确认,确保理解无误、无遗漏。
3.2 主要工作产品
该阶段最重要的产出是两个文档:
- 软件需求规格说明书(SRS):对软件功能、性能、约束的正式描述,是后续设计、测试与验收的基准;
- 用户手册(初稿):从用户视角描述系统将如何使用,帮助尽早暴露理解偏差。
3.3 结构化分析的核心:数据字典
在结构化分析中,**数据字典(Data Dictionary)**是贯穿全局的"词汇表",它是对系统中各类数据元素的精确定义。具体来说,它描述:
- 数据流:数据在系统中流动的路径与内容;
- 加工:对数据施加的变换;
- 数据文件:数据存储;
- 数据元素:不可再分的最小数据单位;
- 数据源点 / 数据汇点:数据的外部来源与去向。
配合数据流图(DFD)使用,数据字典让每个出现在图上的名字都有据可查。
3.4 数据结构描述
除了字典式的条目定义,还可用图形化方式描述复杂数据结构:
- 定义式:用一套符号(如
=表示"定义为"、+表示"与"、[]表示"或"、{}表示"重复")书写数据的组成;
- Warnier 图:以树形层次表达数据的包含与重复关系,适合描述有嵌套结构的数据。
3.5 三种建模视角
结构化分析从三个侧面刻画系统:
建模类型 | 回答的问题 | 常用工具 |
功能建模 | 系统"做什么" | 数据流图 DFD |
数据建模 | 数据"是什么、如何关联" | E-R 图 |
行为建模 | 系统"如何随事件响应" | 状态转换图 |
三者结合,才能完整描述一个系统。
3.6 决策树与决策表
当某个"加工"的逻辑依赖于多个条件的组合时,自然语言容易遗漏分支。此时可用:
- 决策树:用树形结构展示"条件 → 动作",直观易读;
- 决策表:用表格罗列所有条件组合及其对应动作,确保不重不漏。
本章小结
需求分析的目标,是把模糊的用户愿望转化为无歧义的规格说明。数据字典是结构化分析的"中枢",三种建模(功能 / 数据 / 行为)提供完整视图,而决策树与决策表则驯服了复杂的条件逻辑。下一篇,我们进入如何把需求"设计"成可实现的方案。
第四章 结构化设计方法
需求告诉我们"要造什么",设计回答"怎么造"。结构化设计(SD)把需求模型转化为由模块组成的软件结构,是连接分析与编码的桥梁。
4.1 软件设计的五项原则
良好的设计不是凭感觉,而是遵循一组原则:
①分而治之:把大问题拆成小模块,逐个击破; ②模块独立性:追求低耦合、高内聚——模块之间联系尽量弱(耦合低),模块内部联系尽量紧(内聚高); ③提高抽象层次:用概念而非细节思考问题,降低复杂度; ④复用性设计:把通用部分抽出来,便于多处复用; ⑤灵活性设计:预留应变空间,使需求变更时改动最小。
其中"低耦合、高内聚"是评判模块质量的黄金标准。
4.2 模块结构
软件被组织为模块的层次结构:上层模块调用下层模块,并通过接口传递数据。结构化设计的目标,就是从需求阶段的数据流图(DFD)导出这样一张模块结构图。
4.3 基于数据流方法的设计过程
从 DFD 导出模块结构,有一套固定步骤:
- 复查并精化数据流图;
- 确定数据流图中数据流的类型(变换型 or 事务型);
- 导出初始的软件结构图;
- 逐级分解,细化各层模块;
- 精化软件结构;
- 导出接口描述和全局数据结构。
4.4 变换型数据流
最常见的是变换型数据流,其工作过程可概括为三步:
- 取得数据:从外部获取输入;
- 变换数据:在"变换中心"进行核心加工;
- 给出数据:将结果输出。
对应地,模块结构也分为输入枝、变换中心、输出枝三部分,变换分析就是据此"映射"出初始结构。
4.5 软件模块结构的改进方法
初版结构往往粗糙,需要反复优化:
- 模块功能完善化:每个模块应有完整的入口 / 出口、错误处理;
- 消除重复功能:抽取公共部分,改善结构;
- 作用范围应在控制范围以内:模块能影响的范围,不应超出它能直接调用的范围;
- 减少高扇出、增大扇入:避免一个模块管太多下属(扇出过高),鼓励被多处复用(扇入增大);
- 避免病态连接(如模块间出现内容耦合等非法依赖);
- 模块大小适中:过大难懂、过小琐碎,需平衡。
4.6 接口设计的三方面
接口设计关注"模块如何与外界对话",主要包括:
- 模块或软件构件之间的接口;
- 软件与其他软硬件系统之间的接口;
- 软件与人之间的接口(即用户界面)。
4.7 用户界面的特性
好的界面应具备:
- 可使用性:易学、易用、高效;
- 灵活性:能适应不同水平用户的习惯;
- 可靠性:操作可恢复、不致数据丢失。
4.8 程序流程图与基本控制结构
详细设计常用程序流程图描述处理逻辑,它建立在五种基本控制结构之上:
- 顺序型:依次执行;
- 选择型:二选一(注意:标准基本结构中不应出现两个以上的平行分支,复杂选择应分解);
- 先判定型循环(do-while):先判断,可能一次也不执行;
- 后判定型循环(do-until):先执行,至少一次;
- 多情况选择型:根据值分派到多个分支。
这五种结构保证程序清晰、可读、可验证。
4.9 设计阶段的交付文档
结构化设计最终产出三份关键说明书:
- 概要设计说明书(总体结构、模块划分);
- 详细设计说明书(每个模块的实现算法);
- 数据库设计说明书(数据结构与存储方案)。
本章小结
结构化设计把"需求"翻译成"模块结构"。低耦合高内聚是灵魂,数据流方法是主路,变换分析是核心技巧,而接口与界面设计决定了软件好不好用。下一章,我们换一套截然不同的思路——面向对象。
第五章 面向对象方法学概述
前面几章是"结构化"的世界观:把系统拆成功能与数据流。面向对象(OO)换了一套更贴近人类认知的视角——用"对象"而非"功能"来建模世界。本章先建立 OO 的基本概念,并引出统一的建模语言 UML。
5.1 什么是面向对象
面向对象的本质,可以用一个等式概括:
面向对象 = 对象 + 类 + 继承 + 消息通信
- 对象(Object):客观事物的抽象,既有数据(属性)也有行为(操作);
- 类(Class):具有相同属性和操作的对象的模板;
- 继承(Inheritance):子类复用父类的特征,并可加以扩展;
- 消息通信(Message):对象之间通过发送消息协作,而非直接操纵彼此内部。
封装
封装是 OO 的基石,它包含三层含义:
- 清楚的边界:对象对外只暴露必要部分;
- 接口:外界通过定义好的接口使用对象;
- 受保护的内部实现:内部细节对外部隐藏,可自由修改而不影响使用者。
消息通信
对象之间不互相"翻看内部",而是通过消息请求服务。这种松耦合让系统更易于修改与复用。
5.2 主要的面向对象开发方法
OO 思想提出后,出现过多种方法,各有侧重:
- Booch 方法:提出"微开发"与"宏开发"两层次,强调图形化建模;
- Rumbaugh 方法(OMT):以对象模型、动态模型、功能模型三类模型著称;
- Coad 与 Yourdon 方法:以 OOA / OOD 的五层结构清晰易学见长;
- Jacobson 方法(OOSE):以"用例(Use Case)"驱动,强调从用户视角捕获需求。
后来这些方法被统一吸收进UML与RUP。
5.3 统一建模语言 UML
**UML(Unified Modeling Language)**是 OO 建模的工业标准,其特点包括:
- 统一标准:集各家之大成;
- 面向对象:天然契合 OO 世界观;
- 独立于过程:不绑定特定开发方法;
- 可视化:用图形表达模型;
- 与编程语言的关系:UML 是建模语言,可映射到多种编程语言。
UML 的基本事物
UML 的构造块分为四类事物:结构事物(类、接口、构件等)、行为事物(交互、状态机)、分组事物(包)、注释事物(注解)。
UML 的图
UML 提供多种图,常用有:
- 用例图:主要元素是用例和执行者(Actor);用例之间的关系有泛化、扩展、包含(使用);
- 类图:描述类及类之间的关系,包括关联、聚合、组合、泛化;
- 交互图:含顺序图与协作图,描述对象间的动态协作;
- 状态图:描述对象生命周期,包含初态、终态、中间状态与复合状态;
- 此外还有活动图、对象图、构件图、部署图等。
UML 的关系
类与类之间常见关系:关联、依赖、泛化(继承)、实现。
UML 的三种消息
在交互图中,对象间传递的消息分三类:
- 简单消息:仅表示控制流,不区分同步 / 异步;
- 同步消息:发送方等待接收方处理完再继续;
- 异步消息:发送方发出后不等待,继续执行。
本章小结
面向对象用"对象 + 类 + 继承 + 消息"重新组织了软件的世界,封装保证了模块边界,UML 则提供了一套统一的可视化语言。下一章,我们进入面向对象分析(OOA),学习如何用这套语言把需求落成模型。
第六章 面向对象分析
面向对象分析(OOA)的任务,是从问题域中抽象出对象与类,并建立起能反映用户需求的模型。它上承需求,下启设计。
6.1 确定系统边界
分析的第一步,是划清"系统做什么、不做什么"——即确定系统边界:哪些功能属于系统内部,哪些由外部环境(人、其他系统)承担。边界清晰,模型才不会无限膨胀。
6.2 面向对象分析的三种模型
OOA 通常建立三类相互关联的模型:
- 用例模型:从用户视角描述系统功能(对应功能需求);
- 对象模型:描述问题域中的对象、类及其静态关系(最核心);
- 交互模型(动态模型):描述对象之间如何随时间协作完成用例。
三者中,对象模型是骨架,用例与交互围绕它展开。
6.3 建立用例模型的过程
建立用例模型可分三步:
- 确定业务参与者(Actor):谁会使用系统,或与系统交互;
- 确定业务需求用例(Use Case):参与者期望系统提供什么价值;
- 创建用例图:用图形把参与者、用例及其关系画出来。
6.4 用例的规格说明
一个用例不能只画个圈,还需用文字完整描述。一份规范的用例规格说明通常包括:
- 用例名称;
- 执行者;
- 前置条件(执行前系统须满足的状态);
- 后置条件(执行后系统达到的状态);
- 一个主事件流;
- 零到多个备选事件流(异常或分支情形)。
6.5 对象模型的五个层次
Coad 与 Yourdon 提出,对象模型按抽象程度分为五个层次,自顶向下为:
- 主题层:对模型的分区,便于把握大局;
- 类 - 对象层:识别出系统中的类与对象;
- 结构层:类之间的泛化 / 组合等结构关系;
- 属性层:为每个类定义属性;
- 服务层:为每个类定义操作(方法)。
6.6 建立对象模型的步骤
按上述五层,自顶向下逐步建立对象模型:
- 划分主题:先对问题域分块;
- 确定类与对象:从需求中挑出候选类,剔除冗余;
- 确定结构:建立类间的泛化、关联、聚合关系;
- 确定属性:为每个类填上属性;
- 确定服务:为每个类定义应有的操作。
本章小结
OOA 的核心,是用例模型刻画"功能"、对象模型刻画"静态结构"、交互模型刻画"动态协作"。从确定边界,到建立用例,再到五层对象模型,OOA 把用户语言翻译成了开发者可设计的蓝图。下一章(第七章,补全),我们补上从分析到设计之间关键的"设计基础与体系结构"。
第七章 软件设计基础与体系结构【补全章节】
说明:原笔记在"第六章 面向对象分析"与"第八章 面向对象设计"之间缺了一章。本章按软件工程标准体系补写,作为从分析模型迈向设计模型的过渡。如与你的原课件主题不符,可随时替换为讲义中的对应章节。
7.1 从分析模型到设计模型
需求分析产出的是"问题域模型"(做什么),而设计要产出"解域模型"(怎么做)。两者之间有道鸿沟:分析关心用户的世界,设计关心计算机的世界。本章补上的,正是跨越这道鸿沟所需的设计思维基础。
7.2 模块化与抽象(再认识)
第四章已提出"分而治之"与"提高抽象层次"。这里再强调一点:好的抽象让我们在高层忽略细节、在底层专注细节。抽象与逐步求精是贯穿所有设计方法(结构化与面向对象 alike)的统一法则。
7.3 耦合与内聚(深化)
第四章给出了"低耦合、高内聚"的目标。进一步看:
- 内聚从低到高大致为:偶然内聚 < 逻辑内聚 < 时间内聚 < 过程内聚 < 通信内聚 < 顺序内聚 < 功能内聚;功能内聚最佳;
- 耦合从低到高大致为:非直接耦合 < 数据耦合 < 特征耦合 < 控制耦合 < 外部耦合 < 公共耦合 < 内容耦合;数据耦合最理想,内容耦合最坏。
设计时应追求高内聚、低耦合,避免控制耦合与公共耦合这类"牵一发而动全身"的依赖。
7.4 软件体系结构风格
当模块越聚越多,就需要**体系结构(Architecture)**来规定它们的宏观组织方式。常见风格有:
- 分层风格:自上而下分层,上层调用下层(如表现层 / 业务层 / 数据层);
- 客户端 - 服务器(C/S):客户请求、服务器响应;
- 管道 - 过滤器:数据在阶段间流式传递(如编译器、Unix 管线);
- 事件驱动:组件通过广播 / 订阅事件通信;
- 仓库(数据共享):各模块围绕一个公共数据存储协作;
- 微服务(现代演化):将系统拆为独立部署的小服务。
7.5 设计的质量属性
架构选择要在多个质量属性间权衡:
- 性能、可维护性、可扩展性、可靠性、安全性、可移植性等。
没有"最好"的架构,只有"最适合当前诉求"的架构——这正是设计之为艺术也之为工程的地方。
本章小结(补全)
从分析到设计,需要模块化、抽象、耦合 / 内聚这些通用准则,更需要体系结构来组织全局。理解了分层、C/S、管道 - 过滤器等风格,以及质量属性的权衡,我们才具备进入第八章"面向对象设计"的视野。
第八章 面向对象设计
构件
第八章 面向对象设计
分析回答"做什么",设计回答"怎么做"。面向对象设计(OOD)把 OOA 的对象模型,精化为可实现的方案。本章聚焦设计中的关键概念,以及经典的 Coad 与 Yourdon 设计模型。
8.1 构件与类的区别
初学者常混淆"类"与"构件",二者的核心差别在于抽象层次不同:
- 类是逻辑事务:描述对象的模板,存在于设计与代码中;
- 构件(Component)是计算机结点上的物理抽象:是可独立部署、替换的物理单元。
为了起到"物理抽象"作用,类被实现为构件。构件只向外界暴露它所包含类的某些接口,其余大量接口被封装在构件内部,仅被协作的类使用,对其他构件不可见。这种"信息隐藏"正是构件可独立演化的前提。
8.2 服务与子系统接口
- 服务(Service):一组有公共目的的相关操作;
- 子系统接口:包括操作名、操作参数类型及返回值。
设计子系统时,只需对外公布稳定的接口,内部实现可自由变化。
8.3 封闭 / 开放体系结构
- 封闭体系结构:修改某一层只影响相邻层,变化被"封闭"在局部;
- 开放体系结构:一层的变化可能向上、向下传播。
好的设计倾向于封闭性,以控制变更的传播范围(这与第四章"低耦合"一脉相承)。
8.4 Coad 与 Yourdon 的面向对象设计模型
Coad 与 Yourdon 把 OOD 在逻辑上划分为四个部分,每个部分由"主题、类 - 对象、结构、属性、服务"五个层次组成:
- 问题域部分:直接映射到 OOA 的对象模型;
- 人机交互部分:用户界面;
- 任务管理部分:并发与中断处理;
- 数据管理部分:对象的持久化存储。
下面逐一展开。
8.5 问题域部分的设计
在 OOA 模型基础上,对问题域部分做七步调整:
- 调整需求:根据设计阶段新认识修正模型;
- 复用已有的类:引入可复用的类或构件;
- 把问题域类组合在一起:按主题聚合;
- 增添泛化类以建立类间的协议:用抽象父类统一接口;
- 调整继承的支持级别:视语言能力决定是否用多继承;
- 改进性能:针对瓶颈优化;
- 存储对象:决定哪些对象需要持久化。
8.6 任务管理部分的设计
为处理并发,需识别并定义各类任务(进程 / 线程)。常见任务类型与识别步骤:
- 事件驱动型任务:等待外部事件触发;
- 时钟驱动型任务:按固定周期执行;
- 优先任务:高优先级实时处理;
- 关键任务:关系系统安全的紧要操作;
- 协调任务:协调多个任务间的通信。
设计步骤:① 识别事件驱动任务;② 识别时钟驱动任务;③ 识别优先任务;④ 识别关键任务;⑤ 识别协调任务;⑥ 审查每个任务;⑦ 定义每个任务(含优先级、周期、接口)。
8.7 数据管理部分的设计
对象需要保存到数据库或文件中,关键是把对象关系映射为存储结构:
- 一对一关联的映射:两表各加对方主键,或合并为一表;
- 一对多关联的映射:在"多"方表中加"一"方主键作为外键;
- 继承关系的映射:常用方式有"每类一表""父表 + 子表"或"单表继承";
- 此外还有依赖关系的处理与对象设计(为类补充实现细节)。
本章小结
OOD 的关键在于:用构件封装类、用接口隔离变化,并沿"问题域 / 人机交互 / 任务管理 / 数据管理"四个维度组织设计。任务管理驯服并发,数据管理打通持久化——到这一章,软件已从"蓝图"变成了"可建造的方案"。下一章(第九章,补全)将进入"编码实现"。
第九章 软件实现(编码)【补全章节】
说明:原笔记在"第八章 面向对象设计"与"第十章 软件测试"之间缺了一章。按软件工程体系,设计之后、测试之前必然是"编码实现",本文据此补写,作为补全章节。
9.1 编码的任务
设计解决了"方案",编码解决"落地":把设计文档翻译成某门程序设计语言的可执行代码。编码不是机械转录,而是对设计的再推敲——许多设计缺陷正是在写代码时才暴露。
9.2 程序设计语言的选择
语言直接影响可维护性、性能与开发效率。选择时考虑:
- 问题域适配:数值计算、业务逻辑、系统编程各有擅长语言;
- 可维护性与可读性:优先选表达力强、类型安全的语言;
- 团队与生态:库、框架、人才是否可得。
9.3 编码风格与规范
好的代码"写给下一个人读"。要点:
- 清晰命名、合理缩进、适度注释;
- 函数短小、单一职责;
- 遵循团队统一的编码规范(如命名约定、文件组织)。
9.4 代码评审
在运行测试之前,先用"人"来发现缺陷:
- 代码走查(Walkthrough):作者讲解,同行提意见;
- 正式审查(Inspection):按清单系统化检查,效果显著。
评审能发现大量逻辑与规范问题,成本远低于后期返工。
9.5 调试
调试是定位并修复已暴露缺陷的过程。核心思路:复现 → 定位(二分、日志、断点)→ 假设 → 验证修复 → 回归。切忌"盲目改代码碰运气"。
本章小结(补全)
编码把设计变成现实,但"能跑"不等于"好"。选对语言、守住规范、重视评审与调试,才能产出可维护的代码,为第十章的测试打下基础。
第十章 软件测试
写出代码只是开始,证明代码"做对了"才是测试的职责。测试不是挑刺,而是用可控的投入,把缺陷尽可能挡在交付之前。
10.1 测试的基本概念
测试的目标是以最少资源发现最多错误。一个基本原则是:测试只能证明缺陷存在,不能证明缺陷不存在。因此测试讲究策略与覆盖,而非盲目点击。
10.2 白盒测试
白盒测试(White-box)关注程序内部的逻辑结构,在已知代码的前提下设计用例,追求对代码的覆盖。常见覆盖准则由弱到强:
- 语句覆盖 → 判定覆盖 → 条件覆盖 → 判定 / 条件覆盖 → 条件组合覆盖 → 路径覆盖。
覆盖越强,发现隐藏缺陷的概率越高,但用例数也越多。
10.3 黑盒测试
**黑盒测试(Black-box)**把程序当"黑箱",只依据规格说明、不关心内部实现。常用技术:
- 等价类划分:把输入域分为若干等价类,每类取一代表值,减少冗余用例;
- 边界值分析:重点测试等价类的边界(如 0、最大值、上溢点),因为缺陷最爱藏在边界。
10.4 集成测试(组装测试)
单元测试通过后,需把模块组装起来测接口与协作。组装方式有两种:
- 一次性组装(整体拼装):所有模块一次拼好再测。简单但定位错误难;
- 增殖式组装:边拼边测,包括:
- 自顶向下:先顶层、再逐层向下;
- 自底向上:先底层、再向上;
- 混合增殖式:二者结合,兼顾效率与可控。
10.5 测试策略与步骤
完整的测试自底向上、由小到大分四步:
- 单元测试:针对单个模块(函数 / 类);
- 集成测试:验证模块组装后的接口与协作;
- 确认测试:验证软件是否满足需求(有效性);
- 系统测试:把通过确认测试的软件放进真实运行环境,测性能、安全、兼容等。
本章小结
测试用白盒看内部、黑盒看规格,用增殖式组装化解集成风险,并按"单元 → 集成 → 确认 → 系统"四级层层把关。即便如此,仍会有缺陷流入运行——这就需要第十一章的软件维护。
第十一章 软件维护
软件交付不是终点。据统计,维护成本往往占软件生命周期总成本的绝大部分。理解维护,才能理解"软件为什么这么贵"。
11.1 软件维护的定义
软件维护是指:在软件交付后,为保证软件在一个相当长的时期内正常运行而进行的修改活动。它贯穿运行阶段,是软件生命周期中持续时间最长的部分。
11.2 维护的分类
按修改目的,维护可分为四类:
类型 | 目的 | 占比 |
改正性维护 | 修复运行中暴露的缺陷(bug) | 较小 |
适应性维护 | 使软件适应变化的环境(新 OS、新硬件) | 中等 |
完善性维护 | 扩充功能、提升性能,满足新需求 | 最大 |
预防性维护 | 主动修改以提高未来可维护性 | 较小 |
其中完善性维护占比最大——用户用着用着,总会想要"再加点东西"。这也解释了为何软件总量只增不减、越来越难改。
11.3 维护的代价与启示
维护之所以昂贵,源于"维护性恶化":每次匆忙修改都可能引入新缺陷、堆高复杂度。为降低代价,应在设计与编码阶段就注重可读性、低耦合与文档完整——可维护性是设计出来的,不是维护出来的。
本章小结
软件维护是为保证长期正常运行而做的持续修改,改正性、适应性、完善性(占比最大)、预防性四类各有侧重。它提醒我们:软件的真正成本不在开发,而在它漫长的"后半生"。到这一章,我们走完了从概念到退役的完整软件工程主线。