软件体系结构

AI WARNING

未进行对照审核。

软件设计的核心要义

软件开发不仅仅是编写代码,更是一个复杂问题的求解过程。软件设计(Software Design)正是连接需求与最终实现的关键桥梁。

软件设计定义

软件设计(Software Design)可以被视为一个规划过程,为构建软件系统奠定基础。它是一个将用户需求和项目约束(问题空间)转化为具体软件解决方案(解空间)的创造性活动。

  • 名词:指软件系统的规格说明,描述其结构、组件、接口和行为。
  • 动词:指创建这个规格说明的过程。

为何需要设计?

软件系统,尤其是大型软件,本质上是复杂的。这种复杂性源于:

  1. 问题域的复杂性:现实世界问题本身的错综复杂。
  2. 开发过程管理的困难:协调团队、管理变更等。
  3. 软件的灵活性:软件几乎可以实现任何功能,但也带来了无限的可能性和选择。
  4. 离散系统的行为特性:微小的错误可能导致巨大的行为差异,难以预测。

人类的认知能力是有限的(参考 7±2 法则),无法一次性处理过多的信息。设计通过关注点分离层次化帮助我们管理复杂性。

设计的核心思想:分解与抽象

面对复杂系统,两种最基本的思维工具是:

  1. 分解(Decomposition):将复杂系统拆分成更小、更易于管理的部分(子系统、模块、类)。
  2. 抽象(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
  1. 低层设计(代码设计/Software Construction)

    • 关注点:单个函数或方法内部的逻辑实现。
    • 核心:使用基本的编程语言构造(类型、语句、控制结构)来实现特定的数据结构算法
    • 目标:编写简洁、清晰、正确、高效的代码。屏蔽数据结构和算法的实现细节,提供明确的语义和性能。
    • 本质:将算法思想和数据组织方式转化为具体的代码指令。
  2. 中层设计(模块化设计/OO 设计)

    • 关注点:如何将系统划分为独立的、可协作的单元(如模块、类、包)。
    • 核心:应用模块化信息隐藏封装抽象数据类型面向对象原则(如 SOLID)等。
    • 目标:实现高内聚低耦合,使得模块易于理解、开发、测试、修改和复用。
    • 本质:隐藏单个模块(或类)的内部实现细节,通过定义清晰的接口暴露其功能。
  3. 高层设计(软件体系结构设计)

    • 关注点:系统的整体组织结构,包括主要的部件、部件之间的连接件以及它们的配置
    • 核心:定义系统的宏观结构,满足关键的功能性需求、质量属性和项目约束。
    • 目标:为系统建立一个稳定、健壮的骨架,指导后续的详细设计和实现。
    • 本质:进行更高层次的抽象,将系统视为可交互部件的集合,关注系统级行为和特性。

敏捷视点

  • 良好的高层设计是良好底层设计的基础。
  • 设计直到代码编写和测试完成后才算真正完成。
  • 源代码是重要的设计文档,但通常不是唯一必要的。

软件体系结构基础

软件体系结构是软件系统最高层次的抽象。

软件体系结构定义(综合)

一个程序或计算系统的软件体系结构是指系统的一个或多个结构,它包括软件部件、这些部件的外部可见属性以及它们之间的关系(Relationships,通过连接件 Connector 体现)。它还包括指导其设计和演化的原则(如体系结构风格 Architectural Style)。

  • 部件:承担系统主要计算和状态存储的单元。可以是原始的(直接映射到实现)或复合的(由更小的部件和连接件组成)。
  • 连接件:定义和协调部件之间交互的机制。可以是简单的(如过程调用)或复杂的(如消息队列、事件总线)。连接件是与部件同等重要的一等公民
  • 配置:描述部件和连接件如何组合在一起形成系统拓扑结构。

体系结构的重要性

  1. 沟通媒介:为不同利益相关者(客户、设计师、开发者、测试者、维护者)提供共同的理解基础。
  2. 早期决策:体系结构代表了最早期的设计决策,这些决策对系统质量、开发成本和未来演化具有深远影响。
  3. 可传递的抽象:封装了关于系统组织方式的知识,便于复用(如体系结构风格、模式)。

逻辑视图 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 \to Model (调用业务逻辑)
    • Controller \to View (选择视图)
    • View \to Controller (传递用户输入)
    • Model \to View (状态变更通知,通常通过观察者模式)
    • View \to 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
依赖关系 上层拥有下层引用 视图/控制器拥有模型引用,模型通过事件通知视图

软件体系结构设计过程

设计体系结构是一个迭代的、基于决策的过程,通常包括以下步骤:

  1. 分析关键需求和项目约束

    • 识别核心功能需求
    • 明确非功能性需求(质量属性),如性能、安全性、可靠性、可维护性、可扩展性等。这些往往是体系结构设计的关键驱动力。
    • 理解项目约束,如开发时间、成本、团队技能、技术限制等。
  2. 选择体系结构风格

    • 根据关键需求和约束,选择一个或多个合适的体系结构风格作为基础。例如,需要清晰分层和并行开发可能选择分层风格;需要灵活 UI 和多视图可能选择 MVC。
  3. 进行逻辑(抽象)体系结构设计

    • 初步设计:依据概要功能需求和选定的风格,识别主要的逻辑部件和它们之间的关系。将需求大致分配到这些部件。
    • 评价与改进:使用非功能性需求和项目约束来评估初步设计。识别潜在问题(如性能瓶颈、安全漏洞、违反约束),并进行调整和改进。例如,为满足安全需求增加认证部件,为满足分布式需求引入远程调用机制。
  4. 进行物理(实现)体系结构设计

    • 将逻辑部件映射到具体的实现单元(如包、模块、类库)。
    • 考虑开发包(构件)设计:如何组织代码以实现逻辑设计,同时遵循良好的设计原则。
    • 考虑运行时进程:系统运行时将包含哪些进程,它们如何交互(如客户端/服务器进程)。
    • 考虑物理部署:软件如何部署到硬件节点上,网络拓扑如何。
  5. 完善体系结构设计

    • 细化:对关键部件进行更详细的设计,明确其内部结构和职责。
    • 完善:考虑系统启动、初始化、监控、错误处理、资源管理等横切关注点。
  6. 添加构件接口

    • 精确定义每个部件对外提供的服务接口(API)。接口定义应清晰、完整、稳定。
    • 使用接口规约(包括前置条件、后置条件、语法、语义)来描述接口。
  7. 迭代

    • 体系结构设计不是一次性完成的,需要在整个开发过程中不断迭代和演化。

包(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):包之间的依赖关系图中不允许出现环。
      • 原因:循环依赖导致包无法被独立理解、测试和部署(「牵一发动全身」)。
      • 解决方法
        1. 依赖倒置原则(DIP):引入抽象接口,让高层和低层都依赖抽象,打破循环。
        2. 提取新包:将共同依赖的部分提取到一个新的包中。
    • SDP: 稳定依赖原则(Stable Dependencies Principle):依赖关系应该指向更稳定的方向。一个包不应该依赖于比它更不稳定的包
      • 稳定性度量I=CeCa+CeI = \frac{\text{Ce}}{\text{Ca} + \text{Ce}},其中 Ca\text{Ca} 是传入依赖数(Afferent Couplings),Ce\text{Ce} 是传出依赖数(Efferent Couplings)。I[0,1]I \in [0, 1]I=0I=0 最稳定,I=1I=1 最不稳定。
    • SAP: 稳定抽象原则(Stable Abstractions Principle):包的抽象程度应该与其稳定性成正比。稳定的包应该是抽象的,不稳定的包应该是具体的
      • 抽象度度量A=NaNcA = \frac{\text{Na}}{\text{Nc}},其中 Na\text{Na} 是包内抽象类和接口的数量,Nc\text{Nc} 是包内类的总数。A[0,1]A \in [0, 1]A=1A=1 完全抽象,A=0A=0 完全具体。
      • 主序列(Main Sequence):理想情况下,包应位于 A+I=1A+I=1 的线上。偏离主序列太远可能表示设计问题:
        • Zone of Pain: 具体且稳定的包,难以修改
        • Zone of Uselessness: 抽象但不稳定的包,没人依赖

数据对象: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

体系结构构建与集成

体系结构设计完成后,需要将其转化为实际可运行的系统骨架,并逐步填充内容。

体系结构构建

  1. 创建包结构:根据物理设计创建项目中的目录和包。
  2. 创建重要文件:如配置文件、构建脚本、数据库模式文件等。
  3. 定义接口:编写部件之间的接口代码(如 Java Interface)。
  4. 实现关键需求:实现一两个贯穿多层或涉及核心机制的关键用例(端到端),以验证体系结构的可行性。

集成策略

将独立开发的模块组合成一个整体系统的过程称为集成。常见策略:

  1. 大爆炸式:所有模块开发完成后一次性集成。风险高,问题难定位,不推荐
  2. 增量式:逐步集成模块。
    • 自顶向下:从顶层模块开始,逐层向下集成。需要编写桩程序(Stub)来模拟未完成的下层模块。
      • 优点:早期验证高层逻辑和流程,利于故障定位。
      • 缺点:底层细节验证晚,Stub 开发量可能大。
    • 自底向上:从底层模块开始,逐层向上集成。需要编写驱动程序(Driver)来模拟调用该模块的上层模块。
      • 优点:早期验证底层组件功能,Driver 开发量相对较小。
      • 缺点:高层逻辑和整体流程验证晚。
    • 三明治式:结合自顶向下和自底向上,从两头向中间集成。
  3. 持续集成
    • 核心思想:频繁地(通常每天多次)将开发者的代码集成到主干,并自动进行构建和测试。
    • 要求:版本控制系统、自动化构建工具、自动化测试。
    • 优点:尽早发现集成错误,减少集成风险,提高软件质量和发布速度。是现代软件开发的推荐实践

桩与驱动

在增量集成和测试中,需要模拟缺失的部分:

  • :模拟被调用模块。当测试模块 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 个视图来全面描述体系结构:

  1. 逻辑视图
    • 关注点:系统的功能需求,即系统为用户提供的服务。
    • 元素:类、接口、包、子系统及其关系(关联、继承、依赖)。
    • 读者:设计师、开发者。
    • 模型:类图、对象图、包图、状态图。
  2. 过程视图
    • 关注点:系统的运行时行为,如并发、同步、性能、可伸缩性。
    • 元素:进程、线程、任务及其交互(消息、RPC、事件)。
    • 读者:集成者、系统工程师。
    • 模型:顺序图、通信图、活动图。
  3. 开发视图(也称实现视图 Implementation View)
    • 关注点:软件在开发环境中的静态组织,如代码模块、库、子系统。
    • 元素:包、构件、文件及其依赖关系。
    • 读者:开发者、配置管理者。
    • 模型:包图、构件图。
  4. 物理视图(也称部署视图 Deployment View)
    • 关注点:软件到硬件的映射,系统部署拓扑结构。
    • 元素:物理节点(服务器、设备)、网络连接、部署的软件构件。
    • 读者:系统工程师、运维人员。
    • 模型:部署图。
  5. 场景/用例视图(+1)
    • 关注点:通过少量关键用例或场景将其他四个视图联系起来,描述重要交互序列。
    • 作用:发现体系结构元素,验证和驱动设计,作为测试基础。
    • 读者:所有利益相关者。
    • 模型:用例图、顺序图、活动图。

文档化要点

  • 利用标准模板(如 IEEE 1471/4+1 视图),但根据项目特点裁剪。
  • 使用图形(如 UML 图)和文字相结合。
  • 从多视角出发,满足不同利益相关者的需求。
  • 明确接口规约。
  • 体现对变更的灵活性考虑。

体系结构评审

在设计过程的关键节点对体系结构进行评审,有助于及早发现问题,保证质量。

评审角度

  • 正确性、先进性、可行性:方案是否合理可行?
  • 完整性:是否覆盖了所有关键需求和约束?
  • 合理性:系统组成、接口协调是否合理?模块划分是否得当?
  • 明确性:输入输出参数、接口定义是否清晰?
  • 质量属性:性能、可靠性、安全性等要求是否满足?
  • 一致性:不同视图、不同决策之间是否一致?
  • 文档质量:描述是否清晰、准确、无歧义?

评审方法

  1. 基于检查表:使用预定义的检查项列表逐项核对体系结构设计。简单易行,但可能不够深入。
  2. 基于场景:通过模拟系统在特定场景下的行为来评估体系结构。例如 ATAM。
  3. 基于原型:构建系统骨架或关键部分的原型来验证设计。
  4. 体系结构权衡分析方法(Architecture Tradeoff Analysis Method, ATAM):一种系统化的、基于场景的评审方法,专注于识别体系结构决策对质量属性的影响,并揭示其中的权衡风险敏感点

ATAM 简要步骤

  1. 介绍 ATAM 方法。
  2. 介绍业务驱动因素。
  3. 介绍体系结构。
  4. 识别体系结构方法。
  5. 生成质量属性效用树(将质量目标分解为具体场景)。
  6. 分析体系结构方法(针对场景进行分析)。
  7. 头脑风暴并确定场景优先级(更广泛的利益相关者参与)。
  8. 再次分析体系结构方法。
  9. 提交结果。

通过这些方法,可以在编码阶段之前识别和解决体系结构层面的问题,从而降低项目风险和成本。