💡
  • 体系结构的概念
  • 各体系结构⻛格的优缺点
  • 体系结构设计的过程
  • 6大分包原则
  • 体系结构构件之间接⼝的定义(*必考)
    • 怎么定义, 依据什么 → 逻辑层接口(根据需求) 、数据层接口(根据逻辑层需要)
    • 定义出接口的名字, 参数, 返回值(不一定要是完整代码)
  • 体系结构开发集成测试⽤例
    • Stub和Driver

概念(不重要)

💡
主要关注高层设计, 基于反映软件高层抽象的层次——构件.
描述系统的高层结构, 关注点和设计决策
软件体系结构: 部件, 连接件和配置的集合
  • 部件(原始/复合): 软件体系结构的基本组成单位之一, 承载系统的主要功能(处理, 数据)
    • 原始部件常用的软件实现机制
      原始部件常用的软件实现机制
  • 连接件(原始/复合): 另一个基本组成单位, 定义部件之间的交互, 是连接的抽象表示
    • 原始连接件常用的软件实现机制
      原始连接件常用的软件实现机制
  • 配置: 是对”形式”的发展, 定义了部件以及连接件之间的关联方式, 把它们组织成系统的整体结构. 机制: 通过部件端口与连接件角色相匹配的方式, 将系统中部件和连接件的关系定义为一个关联集合, 形成系统整体结构的一个拓扑描述.
    • 配置机制
      配置机制
一个软件的体系结构规定了系统的计算部件和部件之间的交互.
💡
JAY: 非形式化地描述一下配置的作用.
Gemini: 好的, 我们用大白话来聊聊配置 (Configuration) 在软件体系结构里到底扮演了什么角色, 它有什么用.
你可以把软件体系结构想象成搭乐高积木:
1. 构件 (Components) 是乐高积木块
这些积木块就是你手里的各种零件: 红色的长条、蓝色的方块、带轮子的底盘等等. 它们各自有自己的形状和功能. 在软件里, 它们可以是你的程序模块、数据库、用户界面等等.
2. 连接件 (Connectors) 是连接积木块的方式
连接件就像是积木块之间的那些小凸起和凹槽, 或者专门用来连接的轴、销子. 它们决定了不同积木块之间怎么才能拼起来, 以及拼起来后怎么互相传递力(或信息). 在软件里, 这就像是函数调用、API 接口、消息队列等等.
3. 配置 (Configuration) 则是你的“组装图”和“指导书”
想象一下, 你有一大堆乐高积木块和各种连接件, 但如果你不知道怎么拼, 它们还是一堆散沙. 配置就是那个告诉你怎么把这些积木块和连接件正确地、有目的地拼起来的“组装图”或“指导书”.
它起到的作用主要有以下几点:
  • 告诉你“怎么拼”: 配置明确指出哪些构件应该存在, 它们之间应该用什么连接件连接, 以及连接的顺序和方式. 它定义了整个系统的骨架和布局. 比如, 这张图上说红色的积木要和蓝色的积木用销子连起来, 然后蓝色的积木上面再放一个黄色的盖子.
  • 确保系统能“跑起来”: 配置不仅仅是静态的图, 它还包含运行时的“指令”. 它告诉系统在启动时需要加载哪些模块, 数据应该从哪里来, 服务之间怎么找到彼此. 没有配置, 系统可能根本无法启动, 或者无法正常工作. 就像你光有乐高零件, 没有说明书, 你可能连第一步都不知道怎么开始.
  • 让系统“灵活多变”: 这是配置非常重要的一个作用. 有了配置, 你不用每次改一点功能就去改动和编译代码. 你只需要调整配置, 系统就能适应不同的环境或需求. 比如, 你的乐高汽车, 如果想变成卡车, 你不用重新买一套积木, 可能只需要按照另一个“配置”把轮子换成履带, 或者加个车厢, 它就变了. 在软件里, 这就像你通过修改配置文件, 就能让程序连接到不同的数据库, 或者让一个服务在不同服务器上运行, 而不用修改程序代码本身.
  • 体现“整体性”和“架构意图”: 配置是整个软件系统高层次的抽象. 它不仅仅是构件和连接件的简单罗列, 更是软件设计师对于系统整体结构和运行方式的战略思考的体现. 看到配置, 你就大致能知道这个系统是为了解决什么问题, 它是怎么分工合作的. 就像你看到一份乐高城堡的组装图, 你就知道最终成品会是一个城堡, 而不是一艘飞船.
简单来说, 如果构件是零件, 连接件是连接方式, 那么配置就是“组装方案”. 它决定了你的所有零件和连接方式如何组合成一个有功能的、稳定的、并且可以根据需要调整的整体. 没有它, 构件和连接件再多再好, 也只是一堆散乱的材料而已.

常见体系结构风格

💡
关键: 各种体系结构风格以及优缺点

主程序/子程序风格

将系统组织成层次结构, 包括一个主程序和一系列子程序.
主程序是系统的控制器, 负责调度各子程序的执行; 子程序是局部的控制器, 负责调度子子程序的执行.
notion image
重要设计决策与约束:
  • 基于声明-使用关系建立连接件, 以层次分解的方式建立系统部件, 共同组成层次结构
  • 上层部件可以调用下层部件, 反之则不能(不允许逆向调用)
  • 系统应该是单线程执行, 主程序部件拥有最初执行控制权, 在调用过程中将控制权转移给下层子程序
  • 子程序只能通过上层控制权转移来获得控制权, 可以在执行中将控制权转移给下层的子子程序, 并在自身执行完毕后将控制权交还给上层部件
主要实现机制: 模块实现, 每个子程序都实现为一个模块, 主程序是系统的起始模块(一般为包含main函数的模块). 实现模块被组织为相应的层次结构, 通过导入和导出关系相关联.
逻辑上像结构化编程的方法调用(从main函数开始传递控制权到各函数, 最终返回), 然而是更加高层的抽象(控制权在构件之间的传递), 与结构化编程不在同一层次. 构件内部仍可能是基于面向对象等模式实现的.
  • 优点:
    • 流程清晰, 易于理解. 层次分解严格, 结构组织符合功能分解和分而治之的思维方式
    • 强控制性: 严格的层次分解和严格的控制权转移, 对程序的实际执行过程具备很强的控制能力
  • 缺点:
    • 强耦合: 依赖交互方的接口规格, 可能难以修改或复用
    • 程序调用的连接方式限制了部件之间的数据交互, 可能会使得不同部件使用隐含的共享数据交流, 产生不必要的公共耦合, 破坏正确性控制能力

面向对象式风格

将系统组织为多个独立的对象, 每个对象封装其内部的数据, 基于数据向外提供服务. 不同对象之间通过协作机制共同完成系统任务.
notion image
重要设计决策与约束:
  • 依照对数据的使用情况, 用信息内聚的标准为系统建立对象部件, 每个对象部件基于内部数据对外提供服务接口, 隐藏内部数据的表示
  • 基于方法调用机制建立连接件, 将对象部件连接起来
  • 每个对象负责维护自身数据的一致性和完整性, 以此为基础对外提供正确的服务
  • 每个对象都是一个自治单位, 不同对象之间平级, 没有逐次/从属/层次关系
主要实现机制: 模块实现, 将每个对象部件实例都实现为一个模块, 存在连接的对象部件实例之间存在模块的导入/导出关系.
此处“对象”是构件而不是面向对象分析方法中的”对象”实体, 不在一个抽象层. 模块内部使用面向对象/结构化方法实现均可.
  • 优点
    • 内部实现的可修改性: 封装内部数据, 隐藏内部实现, 可以在不影响外界实现的前提下变更内部实现
    • 易开发, 易理解, 易复用的结构组织, 各单位平等自治, 各自负责自身正确性, 仅通过方法调用连接
  • 缺点:
    • 接口的耦合性: 方法调用连接机制使得它无从消除接口的耦合性
    • 标识的耦合性: 一个对象要和其他对象交互就一定要知道其名字(标识), 导致标识的耦合性
    • 副作用: 引入了面向对象的副作用, 更难实现程序的正确性(重入问题, 并发问题等)

分层风格

根据不同的抽象层次, 将系统组织为层次式结构, 每个层次建立为一个部件, 不同部件之间通常用程序调用方式连接(因此连接件为程序调用机制)
notion image
重要设计决策与约束:
  • 上层调用下层. 部件的抽象层次逐层提升, 每个下层为邻接的上层提供服务, 每个上层将下层作为基础设施使用.
  • 两个层次之间的连接要遵守特定的(成熟稳定标准化的)交互协议. 只要遵守交互协议, 不同部件实例之间是可以互相替换的.
  • 禁止跨层次连接/调用
  • 禁止逆向连接/调用
主要实现机制: 不限粒度的模块实现. 每个层次部件可以表示为一个包, 包内部含有它所有的实现模块. 模块之间使用导入/导出关系连接, 子系统之间使用use关系连接, 层之间使用依赖关系连接. 由于分层风格的连接要遵守特定交互协议, 所以实现中要着重完成层次之间的交互接口定义, 并适当允许层内部实现的多样性.
  • 优点
    • 设计机制清晰易于理解: 将混杂的耦合逻辑分解为几个不同部分, 每个部分变得更简单易于理解
    • 支持并行开发, 只要不破坏交互协议, 层之间开发决策就不会互相产生影响
    • 更好的可复用性和内部可修改性: 只要遵守交互协议, 不同层次部件就能互相替换, 且每个层次可以自由安排内部实现机制
  • 缺点
    • 交互协议难以修改
    • 禁止跨层调用可能产生冗余调用处理, 产生性能损失
    • 难以确定层次的数量和粒度

模型-视图-控制(MVC)风格

以程序调⽤作为连接件, 将系统功能组织为模型、视图和控制三个部件.
notion image
  • 模型: 封装系统的数据和状态信息, 实现业务逻辑, 对外提供数据服务和执行业务逻辑(类似后端)
  • 视图: 封装了用户交互, 提供业务展现, 接收用户行为(类似前端)
  • 控制: 封装系统的控制逻辑, 根据用户行为调用需要执行的业务逻辑和数据更新, 根据执行后的系统状态决定后续业务展现(类似前后端的接口)
重要设计决策与约束:
  • 模型、视图、控制分别是关于业务逻辑、表现和控制的三种不同内容抽象
  • 如果视图需要持续地显示某个数据的状态, 那么它⾸先需要在模型中注册对该数据的兴趣. 如果该数据状态发⽣了变更, 模型会主动通知视图, 然后再由视图查询数据的更新情况
  • 视图只能使⽤模型的数据查询服务, 只有控制部件可以调⽤可能修改模型状态的程序
  • ⽤户⾏为虽然由视图发起, 但是必须转交给控制部件处理. 对接收到的⽤户⾏为, 控制部件可能会执⾏两种处理中的⼀种或两种: 调⽤模型的服务, 执⾏业务逻辑;提供下⼀个业务展现.
  • 模型部件相对独⽴, 既不依赖于视图, 也不依赖于控制. 虽然模型和视图之间存在一个“通知变更”的连接, 但该连接的交互协议是⾮常稳定的, 可以认为是⾮常弱的依赖.
主要实现机制: 为模型, 视图和控制的每个部件实例建立模块实现, 各模块之间存在导入/导出关系, 连接件不需要显式实现.
  • 优点
    • 易开发性: 模型, 视图, 控制分别是关于业务逻辑, 表现和控制的三种不同内容抽象, 设计机制清晰, 便于开发
    • 视图和控制的可修改性: 模型封装了系统的业务逻辑, 所以是三种类型中最复杂的系统部件. MVC中模型是相对独立的, 所以对视图实现和控制实现的修改不会影响到模型实现. 再考虑到业务逻辑通常比业务表现和控制逻辑更加稳定, 所以MVC具有一定的可修改性优势.
    • 适合网络系统开发: 对业务逻辑, 表现和控制的分离使得一个模型可以同时建立并保持多个视图
  • 缺点
    • 复杂性: 分解为三个部分增加了系统的复杂性
    • 视图和控制都依赖于模型, 故模型难以修改
💡
JAY: MVC风格是否与前后端分离的模式类似?如果类似的话,我不太理解它为什么允许视图和模型之间可以不经过控制直接进行交互.
Gemini: MVC(Model-View-Controller) 风格与前后端分离的模式在某些方面有相似之处, 但它们并不是完全相同的概念, 也不能互相替代. 理解它们之间的异同, 以及MVC中为什么允许视图和模型直接交互, 需要我们深入探讨它们的设计目标和演变.
MVC 与 前后端分离的相似之处
它们的核心思想都是职责分离 (Separation of Concerns):
  1. 分层解耦:
      • MVC 将应用程序分为三层: 数据/业务逻辑 (Model)、用户界面 (View)、处理用户输入 (Controller).
      • 前后端分离 将整个系统分为前端(负责用户界面和用户交互)和后端(负责数据存储、业务逻辑和API服务).
  1. 专业化分工:
      • MVC 允许不同的开发者专注于各自的M、V、C部分.
      • 前后端分离 使得前端开发人员专注于UI/UX, 后端开发人员专注于业务逻辑/数据, 提高效率.
  1. 可维护性和可扩展性:
      • 两者都通过分离职责, 使得修改一部分代码时, 对其他部分的影响最小化, 从而提高系统的可维护性和可扩展性.
MVC 与 前后端分离的区别
尽管有相似之处, 但它们是不同层次和不同演化阶段的架构模式:
  1. 层次不同:
      • MVC 是一种用户界面架构模式, 它主要关注一个应用程序内部的UI层如何组织和管理. 它通常存在于单个应用进程内, 或者在前后端未完全分离的“大后端”框架(如传统的Java Web应用、ASP.NET MVC等)中.
      • 前后端分离 是一种系统架构模式, 它关注的是整个分布式系统如何划分为独立的前端和后端服务. 前端和后端通常是独立部署、独立运行的两个(或多个)服务, 它们之间通过API(通常是RESTful API)进行通信.
  1. 通信方式:
      • 在传统的 MVC 应用中, Model、View、Controller 之间通常在同一个进程内通过方法调用或观察者模式进行通信.
      • 前后端分离 的架构中, 前端和后端之间通过 HTTP/HTTPS 请求进行跨进程、跨网络的通信.
  1. 演变:
      • MVC 是一种较早出现的模式, 起源于桌面GUI应用. 随着Web应用的兴起, 它被引入到Web开发中.
      • 前后端分离 是随着富客户端(Ajax、单页应用SPA)和移动应用的普及而逐渐成为主流的架构模式.
为什么 MVC 允许视图和模型之间可以不经过控制器直接交互?
这是 MVC 模式的一个经典(也是常被误解或有争议的)设计特点, 主要基于以下原因:
  1. 观察者模式的运用:
      • 在经典的 MVC 设计中, Model(模型)通常会注册 View(视图)作为它的观察者. 当 Model 中的数据发生变化时, 它会主动通知所有注册的 View, View 收到通知后会去 Model 中拉取(Pull)最新的数据来更新自己.
      • 这种直接交互是单向的: Model 通知 View, View 从 Model 获取数据以进行展示. View 不会修改 Model 中的数据.
      • 这种“View → Model”的直接获取数据的交互, 是为了让视图能够及时地响应模型数据的变化, 而无需每次都通过控制器中转, 从而提高响应速度和简化部分逻辑.
  1. 职责明确化:
      • Model: 负责数据和业务逻辑. 它不关心数据如何被展示, 只关心数据的正确性和一致性.
      • View: 负责数据的呈现. 它关心如何显示 Model 提供的数据, 以及如何接收用户的输入(但将输入转发给 Controller).
      • Controller: 负责处理用户输入和业务逻辑. 它接收 View 传来的用户操作, 调用 Model 进行处理, 并决定更新哪个 View 或导航到哪个 View.
      在这样的职责划分下, View 需要获取 Model 的数据才能进行展示, 这是其核心职责. 如果每次 View 要展示数据都必须通过 Controller 来“请求”Model, 再由 Controller 将数据“传递”给 View, 会增加不必要的间接性. 因此, 直接让 View 观察 Model, 当 Model 变化时 View 自行更新, 是一种更高效和直接的机制.
  1. MVC 模式的变种与演进:
      • 值得注意的是, 经典的 MVC 模式有很多变种, 例如 MVP (Model-View-Presenter)MVVM (Model-View-ViewModel).
      • MVP 模式中, View 和 Model 之间通常不直接交互, 所有交互都通过 Presenter. Presenter 充当了 View 和 Model 之间更严格的协调者.
      • MVVM 模式中, View 和 ViewModel 之间通过数据绑定进行双向交互, ViewModel 再与 Model 交互. View 和 Model 之间也通常不直接交互.
      这些变种的目的往往是为了进一步解耦 View 和 Model, 或解决经典 MVC 在特定场景下的一些痛点(如 View 的测试难度).
总结来说:
  • MVC 允许 View 和 Model 之间进行单向的“读取/观察”交互, 主要是为了让 View 能高效、实时地反映 Model 的数据变化.
  • 这种交互是 Model 通知 View (通过观察者模式), 以及 View 从 Model 中读取数据, 而不是 View 直接修改 Model.
  • 而用户对 View 的操作(例如点击按钮、提交表单), 则会发送给 Controller, 由 Controller 来协调 Model 的业务处理, 并最终可能导致 Model 的数据变化, 进而触发 View 的更新.
因此, 你的疑惑在于“不经过控制器直接交互”可能指向的是 View 对 Model 的读操作(即获取数据), 而不是写操作(即修改数据). 修改数据和复杂业务逻辑的处理, 仍然是由 Controller 来协调 Model 完成的.

体系结构设计过程

  1. 分析关键需求和项目约束
  1. 选择体系结构风格
  1. 进行软件体系结构逻辑设计(抽象)
  1. 依赖逻辑设计进行软件体系结构物理设计(实现)
  1. 完善软件体系结构设计
  1. 定义构件接口
  1. 迭代3~6

物理包设计原则

💡
共同封闭, 共同重用, 稳定依赖, 稳定抽象, 重用发布等价, 无环依赖
面向对象软件设计中包设计的六大原则:
  1. 共同封闭原则CCP(一起变化的类放一起)
    1. 定义: 一个包中的类应该因为相同的原因而发生变化. 也就是说, 包应该对同一种类型的变化负责.
      目的: 将需要一起修改的类放在同一个包中, 以减少多个包同时被修改的可能性.
      示例: 如果两个类都因为 UI 改动而经常被修改, 那么它们应该被放在同一个包中, 而不是一个在 UI 包、一个在业务逻辑包中.
  1. 共同重用原则CRP(一起被使用的类放一起)
    1. 定义: 不应该强迫用户依赖他们不需要使用的东西. 一个包中的类应该是被一起使用的类.
      目的: 避免把不相关的类打包在一起, 导致一个类被使用时整个包都要依赖.
      示例: 如果一个包中包含文件处理类和数据库访问类, 而用户只想使用文件处理功能, 强制引入整个包会带来额外依赖.
  1. 重用发布等价原则REP(可重用=可发布)
    1. 定义: 重用的最小单位应该是发布的最小单位. 换句话说, 只有被组织成可发布形式(如 Jar 包、模块)的代码才是可重用的.
      目的: 鼓励把重用代码做成独立发布的包, 方便版本控制和依赖管理.
      示例: 将常用工具类打包成独立的工具包, 统一发布、管理版本, 方便不同项目复用.
  1. 无环依赖原则ADP(包依赖不能成环)
    1. 定义: 包之间的依赖关系不能形成环. 依赖图必须是有向无环图(DAG).
      目的: 避免包间循环依赖, 提升系统的可维护性和可构建性.
      解决方法: 使用“依赖倒置”或“中介包”来打破循环.
      示例: 如果包 A 依赖 B, B 又依赖 C, 而 C 又依赖 A, 这就是一个环, 应该拆分或重构来打破这种循环.
  1. 稳定依赖原则SDP(只依赖比自己更稳定的包)
    1. 定义: 包应该依赖于比自己更稳定的包. 稳定性越高, 意味着越少发生变化.
      目的: 避免高变动的包被多个包依赖, 导致系统不稳定.
      稳定性衡量: 通过入度(被依赖数)/出度(依赖他人包数)来衡量.
      示例: 工具类包(如 StringUtils)变化少, 应该作为底层依赖, 而不是依赖频繁变化的业务逻辑包.
  1. 稳定抽象原则SAP(稳定包应该是抽象的)
    1. 定义: 越稳定的包, 越应该是抽象的(接口多、实现少), 以方便扩展和替代.
      目的: 弥补稳定性的劣势, 用抽象提高灵活性.
      (稳定性的劣势: 依赖于稳定包的包较多, 稳定包自身修改影响范围较大)
      公式: 包的稳定性 × 包的抽象程度 ≈ 稳定抽象平衡
      示例: 核心接口包稳定性高, 应该主要包含接口(抽象类), 具体实现则放在变化频繁的子包中.

构件之间接口

一定会考: 体系结构构件之间接口的定义
接口怎么定义, 依据什么定义
给需求要能定义出逻辑层和数据层两种接口
  1. 根据分配的需求确定模块对外接口
  1. 初步设计关键类
  1. 编写接口规范
逻辑层接口(与VO关联): 看需求和刺激响应. 刺激是接口的输入参数, 响应是接口的返回值
数据层接口(与PO关联): 看逻辑层对数据的需求. 逻辑层有什么样的需求, 数据层才提供什么样的服务.
接口规范示例
接口规范示例
语法, 前置条件, 后置条件

模块的集成方式

大爆炸式

将所有模块一次性组合在一起.
优点: 短时间内迅速完成集成测试
缺点:
  • 一次试运行就成功的可能性不大
  • 问题的定位和修改比较困难, 许多接口错误容易躲开测试
适用于一个维护型项目或被测试系统较小的情况.

增量式

自顶向下式

对分层次的架构, 先集成和测试上层的模块, 下层的模块使用伪装的具有相同接口的桩. 然后不断加入下层的模块, 再进行测试, 直到所有的模块都被集成进来才结束.
优点:
  • 按深度优先可以首先实现和验证一个完整的功能需求
  • 只需要最顶端一个驱动
  • 有利于故障定位
缺点:
  • 桩的开发量大
  • 底层验证被推迟, 且底层组件测试不充分
适用于控制结构比较清晰稳定, 高层接口变化较小, 底层接口尚未定义或可能经常修改, 控制组件具有较大的技术风险的软件系统.

自底向上式

与自顶向下相反, 从最底层的模块集成测试起, 上层的模块使用伪装的相同接口的驱动来替换.
优点:
  • 对底层组件行为较早验证
  • 底层组件开发可以并行
  • 桩的开发量少
  • 有利于故障定位
缺点:
  • 驱动的开发量大
  • 对高层的验证被推迟, 设计上的高层错误不能被及时发现.
适用于底层接口比较稳定, 高层接口变化比较频繁, 底层组件较早被完成的软件系统.

持续集成

提倡尽早集成和频繁集成.
  • 尽早集成: 不需要总是等待一个模块开发完成才把它集成起来, 而是在开发之初就利用桩集成起来.
  • 频繁集成: 每次完成一些开发任务之后, 就可以用开发结果替换桩中的相应组件, 进行集成与测试.
优点:
  • 防止软件开发中出现无法集成与发布的状态, 因为软件项目在任何时刻都是可以集成和发布的
  • 有利于检查和发现集成缺陷. 最早的版本主要集成了简单的桩, 比较容易做到没有错误. 后续代码逐渐开发完成后, 频繁集成又使得即使出现集成问题也能够尽快发现, 尽快解决.
持续集成的频率很高, 所以手动的集成对软件工程师来说无法接受, 必须利用版本控制工具和持续集成工具.
工具链: 版本控制系统, 持续集成服务器/自动化服务器, 构建工具, 自动化测试工具, 代码质量和静态分析工具, 容器化技术, 容器编排工具, 构件管理/制品库, 通知与协作工具.

体系结构开发集成测试用例

  • 桩: 对分层次的架构自顶向下集成时, 先集成和测试上层的模块, 下层的模块使用伪装的具有相同接口的桩代替. 不断加入下层模块再进行测试, 直至所有模块都被集成进来.
    • notion image
  • 驱动: 自底向上集成时, 从最底层的模块集成测试开始, 测试时上层的模块使用伪装的相同接口的驱动代替.
    • notion image
桩模仿下层模块, 简单实现上层需要的服务, 用于测试上层; 驱动模仿上层模块, 利用下层提供的接口, 用于测试下层.
Loading...