一、概述(Overview)
本案例比较的两个系统具有以下共同特征:
| 特征 | 描述 |
|---|---|
| 代码规模 | 相似,约50万行代码 |
| 产品类型 | 嵌入式消费音频设备 |
| 软件生态 | 成熟,经历多次产品发布 |
| 技术基础 | 基于Linux |
| 开发语言 | C++ |
| 开发人员 | 有经验的程序员,且程序员本身就是架构师 |
核心问题 :条件相同,为何结果迥异?答案在于架构设计。
二、背景(Background)
公司处境:
- 初创公司,压力巨大,需迅速推出新版本
- 延误不可容忍,将导致金融崩溃
- 软件工程师被驱使尽快交付代码
- 代码以一系列疯狂的破折号组合拼凑在一起
关键论断 :糟糕的公司结构和不健康的开发过程将反映在糟糕的软件体系结构中。
三、第一个系统:坏的系统(The Bad One)
3.1 不可理解性(Incomprehensibility)
- 软件系统难以理解 ,实际上无法修改
- 新成员被复杂性吓坏
- 糟糕的设计鼓励更糟糕的设计被固定在上面,因为没有办法以理智的方式扩展设计
3.2 缺乏内聚性(Lack of Cohesion)
内聚性定义:组件内部元素的关联程度,高内聚意味着组件职责单一、明确。
问题表现:
- 系统的组件根本没有内聚性
- 每个组件都应具有单个的、定义良好的角色
- 但实际上每个组件都包含一些不必要的相关功能
3.3 不必要的耦合(Unnecessary Coupling)
耦合定义:组件之间的依赖程度,低耦合意味着组件相对独立。
问题表现:
- 系统各部分耦合如此紧密,以至于如果不创建每个组件,就不可能构建一个骨架系统
- 单个组件中的任何更改都会产生波动,需要对许多依赖组件进行更改
- 低级测试是不可能的
重要结论:紧密耦合会导致代码不稳定。
3.4 原因:没有明确的要求(No Clear Requirements)
- 公司知道想要占领哪个市场 ,但不知道用哪种产品占领
- 程序员急于创建一个通用的基础架构,该架构可能做很多事情(糟糕),而不是精心设计一个能够很好地支持一件事情并且可以扩展的架构
- 在大都会规划的最初阶段,有太多的建筑师
设计原则 :如果你不知道它是什么、它应该做什么,不要设计它。只设计你知道你需要的。
3.5 代码问题(Code Problems)
| 问题 | 说明 |
|---|---|
| 没有通用的设计 | no common design |
| 没有整体项目"风格" | no overall project "style" |
| 没有通用的编码标准 | no common coding standards |
| 没有公共库 | no common libraries |
| 没有通用的惯例 | no common idioms |
| 没有组件、类或文件的命名约定 | no naming conventions |
3.6 后果(Consequences)
- 低质量罕见版本的产品(A low-quality product with infrequent releases)
- 不能适应变化或添加新功能的不灵活的系统(An inflexible system)
- 代码问题(Code problems)
- 人员问题:压力、跳槽(Staffing problems: stress, 跳槽)
- 公司内部混乱的政治(Messy internal company politics)
- 公司缺乏成功(Lack of success for the company)
- 加班(Work overtime)
四、第二个系统:好的系统(The Good One)
4.1 设计的第一步(First Steps of Design)
- 在设计过程的早期 ,建立了主要功能领域
- 考虑了它们各自在系统中的位置
- 充实了初始体系结构,包括实现性能需求所必需的核心线程模型
重点投入 :额外的设计时间花费在系统的核心------音频路径(Audio Path)
4.2 初始架构(Initial Architecture)
┌──────────────────────┐
│ User Interface │ 用户界面
├──────────────────────┤
│ Control Components │ 控制部件
├──────────────────────┤
│ Audio Path │ 音频路径
├──────────────────────┤
│ OS/Audio Codecs │ 操作系统/音频编解码器
└──────────────────────┘
4.3 其他决定(Other Decisions)
| 决策项 | 说明 |
|---|---|
| 支持包的选择 | Choice of supporting libraries |
| 顶级文件结构 | Top-level file structure |
| 如何命名事物 | How to name things |
| 常用编码习惯用法 | Common coding idioms |
| 单元测试框架的选择 | Choice of unit test framework |
| 支持基础设施 | 源代码控制、合适的构建系统、持续集成 |
关键点:一旦团队确定了最初的设计,项目就开始了。
4.4 定位功能(Locating Functionality)
- 从一开始就对系统结构进行了清晰的概述
- 新的功能单元被不断地添加 到代码库的正确功能区域中
架构的作用:
- 帮助你定位功能:添加功能、修改功能或修复功能
- 提供了一个模板,供你将工作放入其中
- 提供了用于导航系统的映射
4.5 一致性(Consistency)
- 清晰的架构设计会导致一致的系统
- 所有的决策都应该在建筑设计的上下文中做出
- 清晰的体系结构有助于减少功能的重复
4.6 架构的增长(Growing the Architecture)
- 软件架构不是一成不变的(not set in stone)
- 如果需要,可以更改它
- 要进行更改,体系结构必须保持简单
- 抵制牺牲简单性的修改
4.7 推迟设计决定(Deferring Design Decisions)
- 如果你不需要就别做任何事
- 只在早期设计重要的东西 ,并将所有剩余的决定推迟到稍后
设计原则 :推迟设计决策,直到你必须做出它们。当你还不知道需求时,不要做出体系结构决策。不要猜测。
4.8 保持质量(Maintaining Quality)
| 方法 | 说明 |
|---|---|
| 配对编程 | Pair programming |
| 代码/设计评审 | 任何非成对编程的代码都需要评审 |
| 单元测试 | 每段代码的单元测试 |
4.9 管理技术债务(Managing Technical Debt)
- 小的代码"瑕疵"(code sins)或设计瑕疵(design warts)被允许进入代码库
- 这些敷衍(fudges)被标记为技术债务
- 并计划稍后修订
4.10 单元测试形状设计(Unit Tests Shape Design)
单元测试带来的优点:
- 能够更改软件的各个部分,而不必担心会破坏过程中的所有其他部分
- 为系统提供一组良好的自动化测试 ,允许你以最小的风险进行基本的体系结构更改
4.11 设计时间(Time for Design)
核心观点:由于时间太短,不可能实现任何有价值的设计。
(意思是:必须投入足够的设计时间,否则无法产出好的架构)
4.12 与设计一起工作(Working With the Design)
- 虽然代码库很大,但它是一致的 ,并且容易理解
- 新程序员可以比较容易地掌握和使用它
重要观点:团队的组织对其生成的代码具有不可避免的影响。随着时间的推移,架构还会影响团队合作的好坏。
4.13 最终架构(The Final Architecture)
┌─────────────────────────────────────┐
│ 用户界面 User Interface │
├─────────────────────────────────────┤
│ 控制层 Control ←→ 外部控制器 │
│ External │
│ controllers │
├──────────┬──────────────────────────┤
│ 存储管理 │ 音频路径 │
│ Storage │ Audio path │
│management│ │
├──────────┴──────────────────────────┤
│ 操作系统/音频编解码器 │
│ OS/Audio codecs │
└─────────────────────────────────────┘
五、可能的考试题目
论述题方向:
- 对比分析题:结合案例,分析导致第一个系统失败的架构问题,并说明第二个系统如何避免这些问题。
- 概念解释题 :
- 解释内聚性(Cohesion)和耦合(Coupling)的概念,并说明它们对软件架构的影响
- 什么是技术债务?如何管理技术债务?
- 原则应用题 :
- 论述"推迟设计决策"原则的含义及其在软件架构设计中的应用
- 为什么说"糟糕的公司结构会反映在糟糕的软件架构中"?
- 实践方法题 :
- 论述保持软件质量的方法(配对编程、代码评审、单元测试)
- 单元测试如何影响软件设计?
- 架构设计题 :
- 一个好的软件架构应具备哪些特征?
- 如何在早期设计阶段建立良好的架构基础?