软件设计基础与体系结构概念
核心问题:软件设计的本质是什么?软件体系结构由哪些要素组成?
1. 为什么需要设计:复杂性的根源
1.1 软件的本质复杂性(Brooks, 1987)
Fred Brooks 在《没有银弹》中指出,软件复杂性是本质属性,不是偶然现象。原因在于:
| 来源 | 说明 |
|---|---|
| 问题域本身复杂 | 现实世界充满规则、约束、边界情况和例外 |
| 灵活性的幻觉 | 软件”什么都能改”,但改动会产生连锁反应 |
| 离散系统难以建模 | 不像连续系统可用微分方程精确刻画 |
关键洞见:和桥梁、建筑不同,软件的复杂性来自问题本身,而不是材料或工艺的局限。
1.2 人类认知的局限(Miller, 1956)
Miller 定律:人类短期记忆一次只能处理 5~9 个信息块(7±2)。
- 一个中等规模 Java 系统可能有 500+ 个类
- 直接面对全局是不可能的
- 必须用系统性方法来降低认知负载
1.3 两种复杂度的区分(SWEBOK 2.1)
1
设计复杂度 = 事物复杂度 + 适配复杂度
| 复杂度类型 | 定义 | 例子(在线书店) |
|---|---|---|
| 事物复杂度 | 问题域本身固有的复杂性,无法消除 | 订单状态转换规则、库存并发控制、支付异常处理 |
| 适配复杂度 | 软件实现带来的额外复杂性,可以优化 | Spring 框架配置、ORM 对象-关系映射、HTTP 协议适配 |
好的设计 = 最小化适配复杂度,即让实现尽可能贴近问题域,不引入多余的间接层。
2. 核心思想:分解与抽象
这是控制复杂性的两把手术刀,必须同时使用。
2.1 分解(Decomposition)
目标:将大问题拆成若干独立可理解的子问题。
1
2
3
4
5
6
7
复杂系统
├── 子系统 1(独立可理解)
├── 子系统 2(独立可理解)
└── 子系统 3
├── 子系统 3.1
├── 子系统 3.2
└── 子系统 3.3
在线书店的分解示例:
1
2
3
4
5
BookstoreSystem
├── com.bookstore.service.book (图书管理)
├── com.bookstore.service.order (订单管理)
├── com.bookstore.service.customer (用户管理)
└── com.bookstore.dao (数据访问)
2.2 抽象(Abstraction)
目标:隐藏内部实现细节,只暴露必要的接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 抽象:调用者只看到接口,不关心实现细节
public interface BookService {
Book findById(String bookId);
List<Book> search(String keyword);
boolean checkStock(String bookId, int qty);
}
// 实现:内部细节(数据库查询、缓存逻辑)被隐藏
public class BookServiceImpl implements BookService {
private final BookDao bookDao;
private final Cache cache; // 调用者不知道这里有缓存
@Override
public Book findById(String bookId) {
// 先查缓存,再查数据库——这些细节对外不可见
return cache.get(bookId)
.orElseGet(() -> bookDao.findById(bookId));
}
// ...
}
2.3 分解与抽象的层次性
两者不是一次性操作,而是递归地在每一层同时使用:
- 分解:把这层的系统拆成若干子系统
- 抽象:每个子系统提供一个干净的接口,隐藏内部
1
2
3
4
5
Level 0: BookstoreSystem(整体)
↓ 分解 + 抽象(定义子系统接口)
Level 1: BookService | OrderService | CustomerService | DAO
↓ 继续分解 + 抽象(OrderService 内部)
Level 2: PaymentProcessor | InventoryChecker | OrderRepository
重要认知:分解和抽象是相辅相成的——分解确定边界,抽象定义接口。缺少抽象的分解只是物理拆分,缺少分解的抽象没有结构。
3. 理解软件设计:定义、演化与关系
3.1 设计的定义
从 Ralph (2009) 的定义提炼:
软件设计(名词):在给定环境、约束和目标下,对软件结构的规格说明。 软件设计(动词):创建这份规格说明的过程。
关键词:规格说明(不是代码本身),有目标(满足需求),有约束(性能、团队、技术栈)。
3.2 设计的演化性(SWEBOK 2.2)
设计不是瀑布式一次完成的,而是持续演化的:
- 需求理解加深 → 设计决策需要修正
- 需求变更 → 设计必须跟随变更(这是常态)
概念完整性(Conceptual Integrity)(Brooks, 《人月神话》):
一个系统所有设计决策应该反映一致的理念。
这意味着:
- 系统应该像是出自一个人的手,而不是委员会拼凑的
- 风格一致性 > 每个模块各自”最优”
3.3 设计、需求分析、编程的关系
这三者是软件开发中不同抽象层次的活动:
| 维度 | 需求分析 | 软件设计 | 编程 |
|---|---|---|---|
| 回答的问题 | 做什么? | 怎么组织? | 怎么实现? |
| 关注层面 | 问题域(现实世界) | 解决方案的结构 | 代码细节 |
| 产出物 | 用例图、概念类图 | 包图、组件图、接口 | Java/Python 代码 |
| 类的粒度 | 概念类(无方法) | 设计类(有接口) | 实现类(有完整代码) |
| 可修改性 | 高 | 中(架构决策难改) | 高(但受架构约束) |
从需求到代码的逐步精化路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
概念类(需求)→ 设计类(架构)→ 实现类(编程)
// 阶段1:需求分析中的概念类(只关心"有什么")
// 概念类 Order: 有订单号、商品、用户,不关心方法
// 阶段2:设计类(关心接口和协作)
public interface OrderService {
Order createOrder(String customerId, List<CartItem> items);
void cancelOrder(String orderId);
OrderStatus getStatus(String orderId);
}
// 阶段3:实现类(关心所有细节)
public class OrderServiceImpl implements OrderService {
private OrderDao orderDao;
private BookService bookService;
private PaymentService paymentService;
@Override
public Order createOrder(String customerId, List<CartItem> items) {
// 具体的业务逻辑、事务管理、异常处理...
for (CartItem item : items) {
if (!bookService.checkStock(item.getBookId(), item.getQty())) {
throw new InsufficientStockException(item.getBookId());
}
}
return orderDao.save(new Order(customerId, items));
}
// ...
}
4. 设计分层:低层 / 中层 / 高层
SWEBOK 将软件设计分为三个层次,本讲聚焦高层(体系结构):
| 层次 | 关注点 | 产出物 | 在线书店案例 |
|---|---|---|---|
| 低层设计 | 单个类的属性、方法、算法 | 设计类图 | Order.calculateTotal() 的实现逻辑 |
| 中层设计 | 包/模块的划分、类的协作 | 包图、交互图 | com.bookstore.service.order 包内的类协作 |
| 高层设计(体系结构) | 系统整体结构、分层、部署 | 组件图、部署图 | 展示层 / 业务层 / 数据层的划分 |
低层和中层设计在后续讲次展开;体系结构(高层)是本讲重点。
5. 软件体系结构的定义:三要素模型
5.1 发展简史
| 年代 | 里程碑 |
|---|---|
| 1972 | Parnas 提出模块分解原则(信息隐藏) |
| 1992 | Perry & Wolf 提出经典公式:Elements + Forms + Rationale |
| 1995 | Shaw 提出部件-连接件-配置模型;Kruchten 提出 4+1 视图 |
| 1998 | SEI(Bass 等)给出迄今最权威的定义 |
| 2000 | IEEE 1471 国际标准化 |
5.2 Perry & Wolf (1992) 的经典公式
1
Software Architecture = { Elements, Forms, Rationale }
| 要素 | 含义 |
|---|---|
| Elements | 处理元素、数据元素、连接元素 |
| Forms | 元素的组织形式和拓扑结构 |
| Rationale | 为什么这样选择的理由 — 这是最容易被忽略但最重要的 |
Rationale(理由) 是这个公式最有价值的部分。很多团队只记录了”做了什么”,没有记录”为什么这么做”,导致后来者无法理解架构决策,也无法安全地演化系统。
5.3 Shaw (1995):部件-连接件-配置模型(最常用)
1
软件体系结构 = { Component(部件), Connector(连接件), Configuration(配置)}
| 要素 | 定义 | Java 中的映射 |
|---|---|---|
| 部件(Component) | 承载系统主要功能的基本单位,包含处理逻辑与数据 | 包(Package)、类集合 |
| 连接件(Connector) | 定义部件间交互的抽象表示 | 接口(Interface)、事件 |
| 配置(Configuration) | 定义部件和连接件的关联方式,组成系统总体结构 | 依赖注入(DI)、工厂模式 |
5.4 在线书店系统的三要素映射(完整示例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ====== 部件(Component):com.bookstore.service.book 包 ======
// 这个包是一个"部件",它封装了图书相关的所有功能
// ====== 连接件(Connector):接口定义部件间的交互契约 ======
public interface BookService {
Book findById(String bookId);
List<Book> search(String keyword);
boolean checkStock(String bookId, int qty);
}
public interface OrderService {
Order createOrder(String customerId, List<CartItem> items);
void cancelOrder(String orderId);
}
// ====== 配置(Configuration):依赖注入定义部件间的拓扑关系 ======
// OrderService 依赖 BookService,这个"连接"由配置决定
public class AppConfig {
public BookService bookService(BookDao bookDao) {
return new BookServiceImpl(bookDao);
}
public OrderService orderService(OrderDao orderDao, BookService bookService) {
// 配置:OrderService 通过 BookService 接口与图书模块交互
// 而不是直接依赖 BookServiceImpl(实现细节)
return new OrderServiceImpl(orderDao, bookService);
}
}
架构概念与 Java 映射总结:
1
2
3
4
5
架构概念 Java 映射
─────────────────────────────
部件(Component) → 包 / 类集合
连接件(Connector)→ 接口(Interface)
配置(Configuration)→ 依赖注入 / 工厂模式
5.5 SEI 权威定义(Bass, Clements & Kazman)
The software architecture of a system is the set of structures needed to reason about the system, which comprise software elements, relations among them, and properties of both.
关键词:“needed to reason about” — 架构是为了让人能理解和推理系统而存在的结构集合,不是所有结构,只是有意义的那些。
5.6 4+1 视图预告(Kruchten, 1995)
一个系统可以从不同角度来描述:
| 视图 | 关注点 | UML 图 |
|---|---|---|
| 逻辑视图 | 功能划分、类和包的组织 | 类图、包图 |
| 开发视图 | 代码模块组织、构建管理 | 组件图 |
| 进程视图 | 并发、同步、性能 | 活动图 |
| 物理视图 | 部署、网络拓扑 | 部署图 |
| 场景视图 | 用例驱动,贯穿以上四个视图 | 用例图 |
4+1 视图的核心价值:不同利益相关人关心不同视图。开发者看逻辑/开发视图,运维看物理视图,QA 看场景视图。
6. 体系结构的重要性
6.1 三个根本理由(Clements, 1996)
| 理由 | 说明 |
|---|---|
| 相互沟通 | 架构图是团队和利益相关人之间的”共同语言” |
| 早期设计决策 | 架构决定了系统的质量属性(性能、安全、可维护性),且很难更改 |
| 可转移的抽象 | 好的架构模式可以在类似项目中复用 |
修改一个类的方法签名,影响范围有限。但把单体系统改为分层系统——几乎等于重写。架构是最难修改的设计决策。
6.2 Conway 定律(1968)
“组织设计系统的方式,被其自身的通信结构所约束。”
实际案例:
| 团队成员 | 负责模块 | 对应包 |
|---|---|---|
| 前端工程师 A | 展示层 | controller |
| 后端工程师 B | 图书+搜索 | service.book |
| 后端工程师 C | 订单+支付 | service.order, service.payment |
| 后端工程师 D | 用户+购物车 | service.customer, service.cart |
| DBA E | 数据层 | dao |
5 人团队 → 5 个模块边界。Conway 定律的推论:若想改变架构,有时需要先改变团队结构。
6.3 利益相关人与关注点
不同人关心同一系统的不同方面:
| 利益相关人 | 主要关注点 | 对架构的影响 |
|---|---|---|
| 顾客 | 可用性、响应速度 | 需要缓存层、CDN |
| 开发团队 | 可维护性、可测试性 | 分层 + 接口隔离 |
| 运维 | 可部署性、可监控性 | 日志框架、健康检查端点 |
| 安全团队 | 数据安全、合规性 | HTTPS、加密存储 |
架构师的核心挑战:在这些相互冲突的关注点之间做权衡,而不是满足某一方的极致要求。
7. 架构气味与技术债务
7.1 什么是架构气味(Architecture Smell)
类比代码坏味道(Code Smell),架构气味是系统结构层面的问题信号:
| 气味类型 | 描述 | 后果 |
|---|---|---|
| 循环依赖 | A → B → C → A 形成环 | 无法独立编译/部署/测试 |
| God Component | 一个包/模块承担过多职责 | 修改波及面大,测试困难 |
| 层违规调用 | 展示层直接访问数据层 | 绕过业务规则,安全风险 |
| Feature Envy | 模块频繁访问另一模块的内部数据 | 高耦合,应考虑职责迁移 |
| Scattered Functionality | 同一功能分散在多个模块 | 修改需同时改多处 |
循环依赖示例(Java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 错误:循环依赖
// 包 service.order 中
import com.bookstore.dao.OrderDao;
import com.bookstore.service.book.BookService; // order 依赖 book
// 包 service.book 中
import com.bookstore.service.order.OrderService; // book 又依赖 order —— 循环!
// ✅ 修复:提取共享接口到独立包(依赖倒置原则)
// 新建 com.bookstore.api 包,定义接口
// order 和 book 都依赖 api,不互相依赖
package com.bookstore.api;
public interface StockChecker {
boolean checkStock(String bookId, int qty);
}
层违规调用示例(Java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 错误:Controller(展示层)直接访问 DAO(数据层)
@RestController
public class OrderController {
@Autowired
private OrderDao orderDao; // 违规!跳过了业务层
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
return orderDao.findById(id); // 没有经过任何业务逻辑校验
}
}
// ✅ 正确:Controller 只依赖 Service(业务层)
@RestController
public class OrderController {
@Autowired
private OrderService orderService; // 通过业务层访问
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
return orderService.getOrderWithPermissionCheck(id); // 业务规则在 Service 中
}
}
7.2 架构技术债务(Architectural Technical Debt)
今天为了赶进度做出的架构妥协,会在未来产生持续的额外成本。
| 维度 | 示例 | 后果 |
|---|---|---|
| 模块化缺失 | 初期不做分层 | 后续版本开发时间成倍增加 |
| 接口设计不足 | 层间直接引用实现类 | 替换数据库需要修改全部代码 |
| 文档缺失 | 不写 ADR | 新成员无法理解架构决策理由 |
| 测试缺失 | 不做集成测试 | 架构退化无法被及时发现 |
真实案例警示:某电商平台单体架构在高并发下崩溃,根因是早期未考虑可伸缩性。修复成本 = 重写整个系统。架构债务终将偿还,晚还比早还贵得多。
8. ADR:架构决策记录
8.1 ADR 是什么
Architecture Decision Record(架构决策记录):记录重要架构决策及其理由的文档,包括:
- 做了什么决策
- 为什么做这个决策
- 考虑过哪些替代方案,为什么拒绝它们(这是最核心的)
8.2 ADR 模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## ADR-001: 采用三层分层架构
**状态**: 已采纳
**背景**:
在线书店需支持 Web 和移动端,业务逻辑需要独立于展示层。
团队5人,使用 Java + Spring Boot。
**决策**:
采用 Presentation → BusinessLogic → Data 三层架构。
**理由**:
- 分层降低耦合,便于团队并行开发
- 满足可维护性需求
- Spring Boot 对分层架构有天然支持
**被拒绝方案**:
- 微服务架构:团队仅 5 人,无 K8s 运维经验,运维成本过高
- 单文件脚本:无法扩展,无法测试
**后果**:
- 层间通过接口通信
- 跨层调用(如 controller 直接访问 dao)需严格禁止
- 后续可在不修改业务层的前提下替换数据层实现
8.3 何时需要写 ADR
- 选择架构风格(分层 vs 微服务 vs 事件驱动)
- 选择关键技术栈(数据库类型、消息队列)
- 定义层间通信方式(同步 vs 异步)
- 确定模块边界划分原则
ADR 的价值不是给当前团队看的,而是给 6 个月后加入的新人、或者 2 年后做演化的团队看的。
9. 代码练习
练习一:实现三层分层架构(基础)
目标:理解部件(Component)、连接件(Connector)、配置(Configuration)三要素在代码中的体现。
背景:为在线书店实现图书搜索功能,要求严格遵守分层约束(Controller → Service → DAO,不可跨层)。
要求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 完成 BookDao 接口和其内存实现 InMemoryBookDao
// (模拟数据库,用 Map 存储即可)
public interface BookDao {
Optional<Book> findById(String id);
List<Book> findByTitleContaining(String keyword);
void save(Book book);
}
// 2. 完成 BookService 接口和 BookServiceImpl
// BookServiceImpl 需要通过构造器注入 BookDao(不要直接 new)
// 要求:search 方法如果 keyword 为空或 null,抛出 IllegalArgumentException
public interface BookService {
Optional<Book> getById(String id);
List<Book> search(String keyword); // 需要参数校验
}
// 3. 完成 BookController,只依赖 BookService,不能直接用 BookDao
// 提示:Controller 只负责参数接收和结果返回,业务逻辑在 Service
public class BookController {
private final BookService bookService;
// 构造器注入...
public void handleSearch(String keyword) { /* ... */ }
public void handleGetById(String id) { /* ... */ }
}
// 4. 在 main 方法中手动完成"配置"(Configuration):
// 组装 DAO → Service → Controller 的依赖链
// 并测试搜索功能
思考问题:
- 如果要把
InMemoryBookDao换成MySQLBookDao,需要修改几处代码?为什么这么少? - 如果 Controller 直接持有 BookDao,换数据库时会发生什么?
练习二:识别并修复架构气味(进阶)
目标:识别循环依赖和层违规,并用依赖倒置原则修复。
以下代码存在两个架构气味,请找出并修复:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 文件1:com.bookstore.service.OrderServiceImpl.java
package com.bookstore.service;
import com.bookstore.dao.BookDao; // 问题①:Service 应该依赖 DAO 接口,
// 但这里想直接调用 DAO 的实现
public class OrderServiceImpl {
private BookDao bookDao; // 气味:Service 层直接依赖 DAO 实现类?
public void createOrder(String bookId, int qty) {
// 直接操作 DAO,绕过了 BookService 的业务逻辑(比如库存检查)
bookDao.decreaseStock(bookId, qty);
}
}
// 文件2:com.bookstore.controller.OrderController.java
package com.bookstore.controller;
import com.bookstore.dao.OrderDao; // 问题②:Controller 直接依赖 DAO
public class OrderController {
private OrderDao orderDao; // 气味:跨层!
public void getOrderHistory(String customerId) {
// 跳过 Service 层,直接查数据库
orderDao.findByCustomerId(customerId);
}
}
提示:
- 气味① 是什么类型?如何修复?
- 气味② 是什么类型?如何修复?
- 修复后,画出(或描述)正确的依赖方向
附:体系结构风格速览(下讲预告)
| 风格 | 核心思想 | 典型应用 |
|---|---|---|
| 分层(Layered) | 每层只依赖下层,单向依赖 | Web 三层架构 |
| MVC | 模型-视图-控制器分离 | Spring MVC |
| 微服务 | 独立部署的小服务,通过 API 通信 | Netflix、Uber |
| 事件驱动 | 发布-订阅解耦,异步通信 | Kafka 消息系统 |
| 面向对象 | 封装、继承、多态 | 通用 Java 系统 |