软件体系结构
AI WARNING
未进行对照审核。
软件设计的核心要义
软件开发不仅仅是编写代码,更是一个复杂问题的求解过程。软件设计(Software Design)正是连接需求与最终实现的关键桥梁。
软件设计定义
软件设计(Software Design)可以被视为一个规划过程,为构建软件系统奠定基础。它是一个将用户需求和项目约束(问题空间)转化为具体软件解决方案(解空间)的创造性活动。
- 名词:指软件系统的规格说明,描述其结构、组件、接口和行为。
- 动词:指创建这个规格说明的过程。
为何需要设计?
软件系统,尤其是大型软件,本质上是复杂的。这种复杂性源于:
- 问题域的复杂性:现实世界问题本身的错综复杂。
- 开发过程管理的困难:协调团队、管理变更等。
- 软件的灵活性:软件几乎可以实现任何功能,但也带来了无限的可能性和选择。
- 离散系统的行为特性:微小的错误可能导致巨大的行为差异,难以预测。
人类的认知能力是有限的(参考 7±2 法则),无法一次性处理过多的信息。设计通过关注点分离和层次化帮助我们管理复杂性。
设计的核心思想:分解与抽象
面对复杂系统,两种最基本的思维工具是:
- 分解(Decomposition):将复杂系统拆分成更小、更易于管理的部分(子系统、模块、类)。
- 抽象(Abstraction):隐藏实现的细节,关注于组件的接口和行为。用户只需了解「做什么」(What),无需关心「怎么做」(How)。
graph TD
subgraph 控制复杂性
direction LR
A[复杂系统] -->|分解| B(简单子系统1);
A -->|分解| C(简单子系统2);
A -->|分解| D(简单子系统…);
E[复杂系统实现] -->|抽象| F(系统接口);
end
style A fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#ccf,stroke:#333,stroke-width:2px
分解与抽象通常是并用且具有层次性的。一个系统可以被分解为子系统,每个子系统又可以进一步分解,每一层分解都伴随着对下一层细节的抽象。
设计的本质:决策过程
软件设计是一个充满决策(Decision Making)的过程。设计师需要在众多可能的方案中进行选择,这些决策受到各种约束(Constraints)的影响,如:
- 需求:功能性、非功能性(质量、性能)。
- 环境:目标平台、操作系统。
- 资源:时间、预算、人力。
- 技术:可用框架、编程语言。
设计决策具有以下特点:
- 跳跃性:从问题到解决方案往往需要创造性思维。
- 多样性:通常存在多个「好」的方案,需要权衡选择。
- 演化性:设计过程是迭代的,一个决策会影响后续决策,且早期决策可能需要后续修正。
- 不可逆性(部分):早期错误决策的影响难以完全消除。
- 概念完整性:一系列决策应保持一致的理念和风格,避免混杂。
设计决策依据
设计师通常依据经验、类似系统、参考模型、设计原则、体系结构风格、设计模式等进行决策。
工程设计 vs. 艺术设计
软件设计融合了工程和艺术的元素:
- 工程性:强调实用、坚固、效率、规范、可预测性、科学化、系统化。倾向于理性主义,希望通过模型、方法、工具来规范过程。
- 艺术性:强调美感、简洁、一致性、用户体验、创新、直觉。倾向于经验主义,承认人的因素(认知局限、易错性、需求变更)并强调灵活性、迭代和验证。
优秀的软件设计需要在两者之间取得平衡。
软件设计的分层
为了更好地管理复杂性,软件设计通常被划分为不同的抽象层次:
graph TD
subgraph 设计层次
direction TB
H(高层设计/体系结构设计) --> M(中层设计/模块与类设计);
M --> L(低层设计/代码设计);
end
subgraph 关注点
H --- HN[部件、连接件、配置、质量属性];
M --- MN[模块、类、接口、协作、OO 原则];
L --- LN[数据结构、算法、类型、语句、控制结构];
end
subgraph 抽象与实现
A(抽象) --> I(实现);
end
style H fill:#lightblue
style M fill:#lightgreen
style L fill:#lightyellow
-
低层设计(代码设计/Software Construction)
- 关注点:单个函数或方法内部的逻辑实现。
- 核心:使用基本的编程语言构造(类型、语句、控制结构)来实现特定的数据结构和算法。
- 目标:编写简洁、清晰、正确、高效的代码。屏蔽数据结构和算法的实现细节,提供明确的语义和性能。
- 本质:将算法思想和数据组织方式转化为具体的代码指令。
-
中层设计(模块化设计/OO 设计)
- 关注点:如何将系统划分为独立的、可协作的单元(如模块、类、包)。
- 核心:应用模块化、信息隐藏、封装、抽象数据类型、面向对象原则(如 SOLID)等。
- 目标:实现高内聚、低耦合,使得模块易于理解、开发、测试、修改和复用。
- 本质:隐藏单个模块(或类)的内部实现细节,通过定义清晰的接口暴露其功能。
-
高层设计(软件体系结构设计)
- 关注点:系统的整体组织结构,包括主要的部件、部件之间的连接件以及它们的配置。
- 核心:定义系统的宏观结构,满足关键的功能性需求、质量属性和项目约束。
- 目标:为系统建立一个稳定、健壮的骨架,指导后续的详细设计和实现。
- 本质:进行更高层次的抽象,将系统视为可交互部件的集合,关注系统级行为和特性。
敏捷视点
- 良好的高层设计是良好底层设计的基础。
- 设计直到代码编写和测试完成后才算真正完成。
- 源代码是重要的设计文档,但通常不是唯一必要的。
软件体系结构基础
软件体系结构是软件系统最高层次的抽象。
软件体系结构定义(综合)
一个程序或计算系统的软件体系结构是指系统的一个或多个结构,它包括软件部件、这些部件的外部可见属性以及它们之间的关系(Relationships,通过连接件 Connector 体现)。它还包括指导其设计和演化的原则(如体系结构风格 Architectural Style)。
- 部件:承担系统主要计算和状态存储的单元。可以是原始的(直接映射到实现)或复合的(由更小的部件和连接件组成)。
- 连接件:定义和协调部件之间交互的机制。可以是简单的(如过程调用)或复杂的(如消息队列、事件总线)。连接件是与部件同等重要的一等公民。
- 配置:描述部件和连接件如何组合在一起形成系统拓扑结构。
体系结构的重要性
- 沟通媒介:为不同利益相关者(客户、设计师、开发者、测试者、维护者)提供共同的理解基础。
- 早期决策:体系结构代表了最早期的设计决策,这些决策对系统质量、开发成本和未来演化具有深远影响。
- 可传递的抽象:封装了关于系统组织方式的知识,便于复用(如体系结构风格、模式)。
逻辑视图 vs. 物理视图
理解体系结构时,区分逻辑和物理视图很重要:
- 逻辑视图:关注系统如何概念性地组织和交互,是较高层次的抽象。例如,模块 A 调用模块 B。
- 物理视图:关注系统如何实际地实现和部署,是较低层次的实现细节。例如,模块 A 通过 RMI 调用部署在另一台服务器上的模块 B。
体系结构主要关注逻辑视图,但也需要考虑物理实现的可行性。
体系结构描述语言(ADL)
ADL 是用于形式化描述软件体系结构的语言,旨在提供比非正式图表更精确、更一致的表示。常见的 ADL 元素包括部件、连接件、端口、角色、配置等。
- 例子:ACME, Wright, Darwin (虽然未广泛普及,但体现了形式化描述的思想)
体系结构风格
体系结构风格(Architectural Style),有时也称为体系结构模式(Architectural Pattern),是一组预定义的体系结构设计原则和约束,为特定类型的问题提供解决方案。它定义了:
- 部件和连接件的类型。
- 它们如何交互的拓扑结构。
- 一组约束条件。
- 隐含的优缺点(影响质量属性)。
常见的体系结构风格包括:
主程序-子程序风格(Main Program and Subroutine)
- 部件:主程序、子程序(函数、过程、模块)。
- 连接件:过程/函数调用。
- 拓扑:层次化结构,通常是单向调用(上层调用下层)。
- 约束:通常单线程执行,控制权按调用层级转移和返回。
- 优点:流程清晰,易于理解,强控制性。
- 缺点:强耦合(依赖接口规格),难以修改和复用,可能存在全局数据耦合问题。
- 应用:简单系统,功能可按层次分解的顺序执行任务。
面向对象风格(Object-Oriented)
- 部件:对象(封装了数据和方法)。
- 连接件:方法调用(消息传递)。
- 拓扑:对象网络,对象间通常是平级关系。
- 约束:对象负责维护自身数据一致性(信息隐藏),通过接口交互。
- 优点:内部实现可修改性好,易开发、理解、复用。
- 缺点:接口耦合,标识耦合,副作用和重入问题可能使正确性验证更难。
- 应用:基于数据信息分解和组织的系统,如图形用户界面、模拟系统。
分层风格(Layered)
- 部件:层,每层是一组相关功能的集合(过程或对象)。
- 连接件:层间调用(通常是下层提供的服务接口),可见性受限。
- 拓扑:线性或环状层次结构。
- 约束:
- 上层只能调用其直接下层提供的服务。
- 禁止跨层调用(如第 I 层不能直接调用 I+2 层)。
- 禁止逆向调用(如第 I 层不能调用 I-1 层)。
- 层间交互需遵守稳定、标准化的协议。
- 优点:关注点分离(每层处理不同抽象级别),支持并行开发,可复用性好,内部可修改性强。
- 缺点:交互协议难以修改,可能引入性能损失(层级调用开销),层数和粒度难以确定。
- 应用:网络协议栈(OSI, TCP/IP),操作系统,复杂业务系统。
模型-视图-控制器风格(Model-View-Controller, MVC)
常用于构建交互式应用,尤其是 Web 应用。
- 部件:
- 模型(Model):封装核心数据和业务逻辑。独立于 UI。
- 视图(View):负责数据的展示,向用户呈现界面。可以有多个视图对应一个模型。
- 控制器(Controller):接收用户输入,解释用户操作,调用模型进行处理,并选择合适的视图进行更新。
- 连接件:方法调用、事件通知(如观察者模式)。
- 拓扑:三角关系。
- Controller Model (调用业务逻辑)
- Controller View (选择视图)
- View Controller (传递用户输入)
- Model View (状态变更通知,通常通过观察者模式)
- View Model (查询状态)
- 约束:
- Model 独立于 View 和 Controller。
- View 查询 Model 状态,但不修改。
- Controller 处理用户输入,修改 Model。
- 优点:
- 分离关注点:业务逻辑、数据展示、用户交互分离。
- 易修改性:修改视图或控制器不影响模型。
- 多视图支持:同一模型可对应多个视图。
- 并行开发。
- 缺点:
- 复杂性增加:引入了更多组件和交互。
- 模型修改困难:视图和控制器都依赖模型。
- 应用:Web 应用框架(如 Spring MVC, Ruby on Rails),GUI 应用。
graph TD
subgraph MVC
C(Controller) -- "选择视图" --> V(View);
V -- "用户输入" --> C;
C -- "调用业务逻辑/修改状态" --> M(Model);
M -- "状态变更通知(Observer)" --> V;
V -- "查询状态" --> M;
end
style M fill:#f9f,stroke:#333,stroke-width:2px
style V fill:#9cf,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
分层 vs. MVC
特性 | 分层风格 | MVC 风格 |
---|---|---|
主要分离 | 按抽象层次(表示层、业务逻辑层、数据层) | 按职责(数据/逻辑、展示、控制) |
交互 | 通常是严格的单向依赖(上层依赖下层) | 循环依赖(通过观察者模式等解耦) |
修改影响 | 修改一层接口可能影响上层 | 修改模型可能影响视图和控制器 |
适用场景 | 通用系统结构,网络通信 | 交互式系统,Web 应用,GUI |
依赖关系 | 上层拥有下层引用 | 视图/控制器拥有模型引用,模型通过事件通知视图 |
软件体系结构设计过程
设计体系结构是一个迭代的、基于决策的过程,通常包括以下步骤:
-
分析关键需求和项目约束
- 识别核心功能需求。
- 明确非功能性需求(质量属性),如性能、安全性、可靠性、可维护性、可扩展性等。这些往往是体系结构设计的关键驱动力。
- 理解项目约束,如开发时间、成本、团队技能、技术限制等。
-
选择体系结构风格
- 根据关键需求和约束,选择一个或多个合适的体系结构风格作为基础。例如,需要清晰分层和并行开发可能选择分层风格;需要灵活 UI 和多视图可能选择 MVC。
-
进行逻辑(抽象)体系结构设计
- 初步设计:依据概要功能需求和选定的风格,识别主要的逻辑部件和它们之间的关系。将需求大致分配到这些部件。
- 评价与改进:使用非功能性需求和项目约束来评估初步设计。识别潜在问题(如性能瓶颈、安全漏洞、违反约束),并进行调整和改进。例如,为满足安全需求增加认证部件,为满足分布式需求引入远程调用机制。
-
进行物理(实现)体系结构设计
- 将逻辑部件映射到具体的实现单元(如包、模块、类库)。
- 考虑开发包(构件)设计:如何组织代码以实现逻辑设计,同时遵循良好的设计原则。
- 考虑运行时进程:系统运行时将包含哪些进程,它们如何交互(如客户端/服务器进程)。
- 考虑物理部署:软件如何部署到硬件节点上,网络拓扑如何。
-
完善体系结构设计
- 细化:对关键部件进行更详细的设计,明确其内部结构和职责。
- 完善:考虑系统启动、初始化、监控、错误处理、资源管理等横切关注点。
-
添加构件接口
- 精确定义每个部件对外提供的服务接口(API)。接口定义应清晰、完整、稳定。
- 使用接口规约(包括前置条件、后置条件、语法、语义)来描述接口。
-
迭代
- 体系结构设计不是一次性完成的,需要在整个开发过程中不断迭代和演化。
包(Package)设计原则
在进行物理设计,特别是组织代码包时,遵循一些原则有助于管理依赖关系,提高可维护性和可复用性:
- 包内聚原则(Package Cohesion):指导如何将类组织到包中。
- REP: 重用发布等价原则(Reuse-Release Equivalency Principle):重用的单元应等于发布的单元。一起重用的类应放在同一个包中。
- CCP: 共同闭包原则(Common Closure Principle):包中所有类对于同一类性质的变化应该是共同封闭的。一个变化若对包产生影响,则应只影响包内的类。一起修改的类应放在同一个包中。
- CRP: 共同重用原则(Common Reuse Principle):包中的类应该被一起重用。如果重用了包中的一个类,就应该重用包中的所有类。不要强迫用户依赖他们不需要的东西。
CCP vs. CRP
CCP 倾向于使包更大(包含所有可能一起变化的东西),有利于维护者。
CRP 倾向于使包更小(只包含必须一起重用的东西),有利于重用者。
需要在两者间权衡,项目早期可能侧重 CCP,稳定后可重构以侧重 CRP。
- 包耦合原则(Package Coupling):处理包之间的关系。
- ADP: 无环依赖原则(Acyclic Dependencies Principle):包之间的依赖关系图中不允许出现环。
- 原因:循环依赖导致包无法被独立理解、测试和部署(「牵一发动全身」)。
- 解决方法:
- 依赖倒置原则(DIP):引入抽象接口,让高层和低层都依赖抽象,打破循环。
- 提取新包:将共同依赖的部分提取到一个新的包中。
- SDP: 稳定依赖原则(Stable Dependencies Principle):依赖关系应该指向更稳定的方向。一个包不应该依赖于比它更不稳定的包。
- 稳定性度量:,其中 是传入依赖数(Afferent Couplings), 是传出依赖数(Efferent Couplings)。, 最稳定, 最不稳定。
- SAP: 稳定抽象原则(Stable Abstractions Principle):包的抽象程度应该与其稳定性成正比。稳定的包应该是抽象的,不稳定的包应该是具体的。
- 抽象度度量:,其中 是包内抽象类和接口的数量, 是包内类的总数。, 完全抽象, 完全具体。
- 主序列(Main Sequence):理想情况下,包应位于 的线上。偏离主序列太远可能表示设计问题:
- Zone of Pain: 具体且稳定的包,难以修改
- Zone of Uselessness: 抽象但不稳定的包,没人依赖
- ADP: 无环依赖原则(Acyclic Dependencies Principle):包之间的依赖关系图中不允许出现环。
数据对象:VO 与 PO
在分层架构中,不同层之间传递数据时,常使用特定的数据对象:
- PO(Persistent Object):持久化对象,通常与数据库表结构对应。主要用于数据存储和访问层(Data Layer)。它们是数据的载体,一般只包含 getter/setter 方法,很少有业务逻辑。常使用 POJO (Plain Old Java Object) 实现。
- VO(Value Object):值对象,用于在层之间(尤其是业务逻辑层 Logic Layer 和表示层 Presentation Layer 之间)传递数据。VO 的结构通常根据消费方(如视图)的需求来设计,可能包含来自多个 PO 的数据,或者只包含 PO 的部分数据。VO 强调值的概念,有时设计为不可变的。
graph LR
direction TB
P(Presentation Layer) -- Uses --> L(Logic Layer);
L -- Uses --> D(Data Layer);
L -- VO --> P;
D -- PO --> L;
L -- PO --> D;
style P fill:#lightblue
style L fill:#lightgreen
style D fill:#lightyellow
体系结构构建与集成
体系结构设计完成后,需要将其转化为实际可运行的系统骨架,并逐步填充内容。
体系结构构建
- 创建包结构:根据物理设计创建项目中的目录和包。
- 创建重要文件:如配置文件、构建脚本、数据库模式文件等。
- 定义接口:编写部件之间的接口代码(如 Java Interface)。
- 实现关键需求:实现一两个贯穿多层或涉及核心机制的关键用例(端到端),以验证体系结构的可行性。
集成策略
将独立开发的模块组合成一个整体系统的过程称为集成。常见策略:
- 大爆炸式:所有模块开发完成后一次性集成。风险高,问题难定位,不推荐。
- 增量式:逐步集成模块。
- 自顶向下:从顶层模块开始,逐层向下集成。需要编写桩程序(Stub)来模拟未完成的下层模块。
- 优点:早期验证高层逻辑和流程,利于故障定位。
- 缺点:底层细节验证晚,Stub 开发量可能大。
- 自底向上:从底层模块开始,逐层向上集成。需要编写驱动程序(Driver)来模拟调用该模块的上层模块。
- 优点:早期验证底层组件功能,Driver 开发量相对较小。
- 缺点:高层逻辑和整体流程验证晚。
- 三明治式:结合自顶向下和自底向上,从两头向中间集成。
- 自顶向下:从顶层模块开始,逐层向下集成。需要编写桩程序(Stub)来模拟未完成的下层模块。
- 持续集成
- 核心思想:频繁地(通常每天多次)将开发者的代码集成到主干,并自动进行构建和测试。
- 要求:版本控制系统、自动化构建工具、自动化测试。
- 优点:尽早发现集成错误,减少集成风险,提高软件质量和发布速度。是现代软件开发的推荐实践。
桩与驱动
在增量集成和测试中,需要模拟缺失的部分:
- 桩:模拟被调用模块。当测试模块 A,而 A 需要调用模块 B,但 B 尚未完成时,用 Stub B 代替。Stub B 接收调用,可能返回预设的简单数据或执行简单逻辑。
- 驱动:模拟调用模块。当测试模块 B,而 B 需要被模块 A 调用,但 A 尚未完成时,用 Driver A 代替。Driver A 设置测试环境,调用 B,并可能验证 B 的返回结果。
graph TD
D(驱动 Driver) -- 调用 --> T(被测模块);
T -- 调用 --> S(桩 Stub);
S -- 返回模拟结果 --> T;
T -- 返回结果 --> D;
style T fill:#lightgreen,stroke:#333,stroke-width:2px
style D fill:#lightblue,stroke:#333,stroke-width:1px
style S fill:#lightyellow,stroke:#333,stroke-width:1px
体系结构文档化
将体系结构设计的结果记录下来,形成文档,对于沟通、维护和演化至关重要。
IEEE 1471 (现 ISO/IEC/IEEE 42010)
该标准提供了描述软件密集系统体系结构的框架,强调视点和视图的概念。
- 视点:定义了描述体系结构特定方面(关注点)的约定,包括使用的模型、符号和规则。
- 视图:根据某个视点对系统体系结构进行的表示。
4+1 视图模型(Philippe Kruchten)
一个广泛使用的多视图模型,通过 5 个视图来全面描述体系结构:
- 逻辑视图
- 关注点:系统的功能需求,即系统为用户提供的服务。
- 元素:类、接口、包、子系统及其关系(关联、继承、依赖)。
- 读者:设计师、开发者。
- 模型:类图、对象图、包图、状态图。
- 过程视图
- 关注点:系统的运行时行为,如并发、同步、性能、可伸缩性。
- 元素:进程、线程、任务及其交互(消息、RPC、事件)。
- 读者:集成者、系统工程师。
- 模型:顺序图、通信图、活动图。
- 开发视图(也称实现视图 Implementation View)
- 关注点:软件在开发环境中的静态组织,如代码模块、库、子系统。
- 元素:包、构件、文件及其依赖关系。
- 读者:开发者、配置管理者。
- 模型:包图、构件图。
- 物理视图(也称部署视图 Deployment View)
- 关注点:软件到硬件的映射,系统部署拓扑结构。
- 元素:物理节点(服务器、设备)、网络连接、部署的软件构件。
- 读者:系统工程师、运维人员。
- 模型:部署图。
- 场景/用例视图(+1)
- 关注点:通过少量关键用例或场景将其他四个视图联系起来,描述重要交互序列。
- 作用:发现体系结构元素,验证和驱动设计,作为测试基础。
- 读者:所有利益相关者。
- 模型:用例图、顺序图、活动图。
文档化要点
- 利用标准模板(如 IEEE 1471/4+1 视图),但根据项目特点裁剪。
- 使用图形(如 UML 图)和文字相结合。
- 从多视角出发,满足不同利益相关者的需求。
- 明确接口规约。
- 体现对变更的灵活性考虑。
体系结构评审
在设计过程的关键节点对体系结构进行评审,有助于及早发现问题,保证质量。
评审角度
- 正确性、先进性、可行性:方案是否合理可行?
- 完整性:是否覆盖了所有关键需求和约束?
- 合理性:系统组成、接口协调是否合理?模块划分是否得当?
- 明确性:输入输出参数、接口定义是否清晰?
- 质量属性:性能、可靠性、安全性等要求是否满足?
- 一致性:不同视图、不同决策之间是否一致?
- 文档质量:描述是否清晰、准确、无歧义?
评审方法
- 基于检查表:使用预定义的检查项列表逐项核对体系结构设计。简单易行,但可能不够深入。
- 基于场景:通过模拟系统在特定场景下的行为来评估体系结构。例如 ATAM。
- 基于原型:构建系统骨架或关键部分的原型来验证设计。
- 体系结构权衡分析方法(Architecture Tradeoff Analysis Method, ATAM):一种系统化的、基于场景的评审方法,专注于识别体系结构决策对质量属性的影响,并揭示其中的权衡、风险和敏感点。
ATAM 简要步骤
- 介绍 ATAM 方法。
- 介绍业务驱动因素。
- 介绍体系结构。
- 识别体系结构方法。
- 生成质量属性效用树(将质量目标分解为具体场景)。
- 分析体系结构方法(针对场景进行分析)。
- 头脑风暴并确定场景优先级(更广泛的利益相关者参与)。
- 再次分析体系结构方法。
- 提交结果。
通过这些方法,可以在编码阶段之前识别和解决体系结构层面的问题,从而降低项目风险和成本。