体系结构风格与设计过程
前置知识:架构三要素(组件、连接器、约束)、设计分层、技术债务与 ADR
1. 体系结构风格:是什么,为什么需要
定义
体系结构风格(Architectural Style) 是一组设计决策的模板,规定了三件事:
1
风格 = 组件词汇 + 连接器词汇 + 拓扑约束
- 组件词汇:这种风格里有哪些”构建块”(如层、过滤器、服务)
- 连接器词汇:组件之间怎么交互(如过程调用、管道、事件总线)
- 拓扑约束:组件能怎么连(如”分层只能调用直接下层”)
为什么需要风格?
| 目的 | 解释 |
|---|---|
| 经验复用 | 经过工业验证的设计方案,不用从零摸索 |
| 通用词汇 | 说”三层架构”,团队所有人立刻理解意图 |
| 质量属性权衡明确 | 每种风格有已知的优劣(如分层牺牲性能换可维护性) |
关键洞察:风格不是”编程规范”,是系统级的组织原则。选错风格,代码再整洁也救不了架构。
2. 风格 vs 设计模式:粒度之别
这是常见的混淆点,必须厘清:
| 维度 | 体系结构风格(Style) | 设计模式(Pattern) |
|---|---|---|
| 作用范围 | 系统全局组织方式 | 局部重复问题的解决方案 |
| 粒度 | 定义系统大部分的结构 | 应用于系统中单个元素 |
| 示例 | 分层、微服务、管道-过滤器 | 观察者、工厂、代理 |
| 能否共存 | 一个系统通常选一种主风格 | 多个模式可以同时存在 |
案例:在线书店中风格与模式共存
1
2
3
4
主风格:三层分层架构(全局)
└─ 展示层内部:MVC 模式
└─ Model 通知 View:Observer 模式
└─ 数据访问层:DAO 模式
3. 主要风格一览与适用场景
SWEBOK 将架构风格分为六大类:
| # | 类别 | 包含风格 |
|---|---|---|
| 1 | General Structures | 分层、管道-过滤器、黑板 |
| 2 | Distributed Systems | 客户端-服务器、n-tier、REST、微服务 |
| 3 | Method-Driven | 面向对象、事件驱动、数据流 |
| 4 | User-Computer Interaction | MVC、PAC |
| 5 | Adaptive Systems | 微内核、反射 |
| 6 | Virtual Machines | 解释器、规则引擎 |
各风格快速对比
| 风格 | 组件 | 连接器 | 典型场景 | 核心优势 | 核心劣势 |
|---|---|---|---|---|---|
| 主程序-子程序 | 主程序、子程序 | 过程调用 | 小脚本、传统系统 | 结构清晰、易调试 | 耦合高、扩展性差 |
| 分层 | 层 | 过程调用 | 企业信息系统 | 可维护、可测试 | 性能开销、级联修改 |
| 面向对象 | 对象/类 | 方法调用 | 现代软件框架 | 封装、继承、多态 | 需良好设计 |
| 管道-过滤器 | 过滤器 | 管道(数据流) | 编译器、数据处理 | 极松耦合、可并行 | 难以交互 |
| MVC | Model/View/Controller | 事件+直接调用 | Web 应用、GUI | 交互逻辑分离 | Controller 易膨胀 |
| 微服务 | 独立服务 | REST/消息队列 | 大规模互联网系统 | 独立部署、弹性扩展 | 运维复杂度高 |
| 事件驱动 | 事件源、监听器 | 事件总线 | GUI、消息系统 | 松耦合、异步 | 控制流难追踪 |
4. KWIC 实验:同一问题,五种风格
背景:Parnas 1972 年提出的经典实验,用同一个问题考察不同风格的优劣。
问题描述
KWIC(Key Word in Context,上下文关键词索引):
- 输入:”Software Architecture”
- 处理:对每行做循环位移(首词移到末尾)
- 输出(按字母序):”Architecture Software”, “Software Architecture”
五种实现对比
| 风格 | 数据共享方式 | 耦合程度 | 核心特征 |
|---|---|---|---|
| MS1(主程序-子程序,索引存储) | 共享存储 + 索引 | 紧 | 空间高效,数据无隐藏 |
| MS2(主程序-子程序,完整字符串) | 共享存储 + 完整字符串 | 紧 | 直观,空间开销大 |
| OO(面向对象) | 接口访问,内部隐藏 | 松 | 信息隐藏,易变更数据表示 |
| PF(管道-过滤器) | 管道传输,无共享 | 极松 | 增量处理,仅限批处理 |
| ES(事件系统) | 抽象数据空间 | 松 | 灵活扩展,控制流难追踪 |
变更场景分析(重点)
这是实验最有价值的部分——考察各风格对不同变更的适应能力:
| 变更类型 | MS1/MS2 | OO | PF | ES |
|---|---|---|---|---|
| 数据表示变更(索引→完整字符串) | 改所有模块 | 仅改内部实现 | 改过滤器链 | 调整事件策略 |
| 算法变更(按需生成位移) | 困难(数据耦合) | 中等 | 改过滤器即可 | 中等 |
| 功能扩展(过滤噪声词) | 直接修改共享数据 | 中等 | 新增过滤器 | 调整监听器 |
核心结论:没有最好的风格,只有最适合需求的风格。
- MS:空间敏感、变更少 → 选 MS
- OO:数据表示可能独立演化 → 选 OO
- PF:数据流式、增量处理 → 选 PF
- ES:功能动态扩展 → 选 ES
5. 分层风格(Layered Architecture)
5.1 核心思想
分层的本质是关注点分离(Separation of Concerns):把”不同层次的抽象”放入不同的层,每层只做一件事,通过接口对外提供服务。
5.2 严格分层 vs 松散分层
| 类型 | 规则 | 优点 | 缺点 | 示例 |
|---|---|---|---|---|
| 严格分层 | 每层只能调用直接下层 | 依赖关系极清晰,修改影响可控 | 性能差(数据必须逐层传递) | OSI 七层网络模型 |
| 松散分层 | 允许跨层调用 | 性能好,开发灵活 | 依赖关系可能变乱 | 大多数企业应用 |
实践建议:大多数企业系统选松散分层,但必须规定哪些层可以跨层(如展示层可直接访问实体层)。
5.3 三层架构详解
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────┐
│ 展示层 Presentation │ ← 接收请求,渲染视图
│ (Controller + View/HTML) │
├─────────────────────────────┤
│ 业务逻辑层 Business Logic │ ← 核心业务规则,协调工作流
│ (Service, Domain Logic) │
├─────────────────────────────┤
│ 数据访问层 Data Access │ ← CRUD,数据映射,事务管理
│ (DAO/Repository + ORM) │
└─────────────────────────────┘
│
[Database]
核心约束(必须遵守):
- 展示层不直接访问数据库
- 业务逻辑层不包含 UI 代码
- 数据访问层不包含业务规则
- 各层通过接口(Interface) 解耦,而不是直接依赖实现类
5.4 各层职责与技术栈
| 层次 | 核心职责 | 典型 Java 技术 |
|---|---|---|
| 展示层 | 接收请求、渲染视图、用户交互 | Spring MVC Controller, Thymeleaf, React |
| 业务逻辑层 | 实现业务规则、协调工作流 | Spring @Service, EJB |
| 数据访问层 | CRUD 操作、数据映射、事务管理 | MyBatis @Mapper, JPA, Hibernate |
5.5 分层的四大优势与代价
优势:
| 优势 | 说明 | 示例 |
|---|---|---|
| 封装性 | 每层内部实现对外隐藏 | MySQL → PostgreSQL 不影响业务层 |
| 关注点分离 | 开发者只需关注本层逻辑 | 前端工程师无需了解 SQL |
| 可替换性 | 整层可被替换而不影响其他层 | Web UI 替换为移动端 UI |
| 可测试性 | 各层可独立单元测试 | Mock 数据层测试业务逻辑 |
代价(工程权衡,务必了解):
- 性能开销:层间调用带来额外的方法调用栈开销
- 级联修改:新增字段可能需要逐层传递(VO/DTO 都要改)
- 过度分层:简单脚本分三层反而增加无谓复杂度
5.6 三层架构代码示例(在线书店)
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ===== 展示层 Controller =====
// 职责:接收 HTTP 请求,调用业务层,返回结果
// 不包含任何业务逻辑,不直接访问数据库
@RestController
@RequestMapping("/api")
public class BookController {
@Autowired
private BookService bookService; // 依赖接口,不依赖实现
@GetMapping("/books/{id}")
public Book getBook(@PathVariable Long id) {
return bookService.findById(id); // 调用业务层
}
}
// ===== 业务逻辑层 Service =====
// 职责:业务规则(如库存检查、折扣计算)
// 不包含 UI 代码,不直接写 SQL
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao; // 依赖 DAO 接口
@Override
public Book findById(Long id) {
Book book = bookDao.selectById(id);
if (book == null) {
throw new BookNotFoundException("Book not found: " + id);
}
return book;
}
// 业务规则示例:购买时检查库存
public void purchase(Long bookId, int quantity) {
Book book = bookDao.selectById(bookId);
if (book.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}
bookDao.decreaseStock(bookId, quantity);
}
}
// ===== 数据访问层 DAO =====
// 职责:封装 SQL/ORM 操作
// 不包含业务规则(如"库存不足"的判断不应该在这里)
@Mapper
public interface BookDao {
@Select("SELECT * FROM book WHERE id = #{id}")
Book selectById(Long id);
@Update("UPDATE book SET stock = stock - #{qty} WHERE id = #{id}")
void decreaseStock(@Param("id") Long id, @Param("qty") int qty);
}
注意:上述代码体现了”上层依赖下层接口”的关键原则。
BookController持有BookService接口(而非BookServiceImpl),使得未来替换实现类无需修改 Controller。
6. MVC 风格
6.1 为什么需要 MVC?
问题:早期 Web 开发(如 PHP/JSP 时代)经常把数据库查询、业务逻辑、HTML 输出混写在一个文件里。
1
2
3
4
5
6
7
8
9
10
// 反模式:一锅炖
$result = mysql_query("SELECT * FROM books");
echo "<html><table>";
while ($row = mysql_fetch_array($result)) {
if ($row['price'] > 100) { // 业务逻辑混在展示代码里
echo "<tr style='color:red'>";
}
echo "<td>" . $row['title'] . "</td>";
}
echo "</table></html>";
问题:数据库结构一变,整个页面要重写;UI 改版时要动业务逻辑;完全无法测试。
MVC 的解决方案:把”数据/逻辑”、”展示”、”交互控制”三件事分开。
6.2 三组件职责
| 组件 | 职责 | 关键约束 |
|---|---|---|
| Model | 管理数据与业务逻辑 | 不依赖 View 和 Controller |
| View | 展示数据给用户 | 只负责展示,不含业务逻辑 |
| Controller | 处理用户输入 | 是 View 与 Model 的协调者 |
6.3 交互流程
1
2
3
4
5
6
7
8
9
用户操作 → Controller
│
├─ 更新 → Model(业务操作/数据变更)
│ │
│ └─ 通知 → View(状态变化通知)
│ │
└─ 选择 → View └─ 查询 → Model(获取最新数据)
│
渲染 → 用户看到更新后的界面
6.4 Observer 模式在 MVC 中的角色
MVC 的 Model → View 通知机制正是 Observer(观察者)模式的体现:
- Model = 被观察者(Subject):维护一个
List<View>观察者列表 - View = 观察者(Observer):注册到 Model,实现
update()方法 - 当 Model 数据变化,调用
notify()通知所有 View 自动刷新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Observer 模式在 MVC 中的体现(简化示意)
public class BookModel {
private List<BookView> observers = new ArrayList<>();
private List<Book> books;
public void attach(BookView view) {
observers.add(view);
}
public void addBook(Book book) {
books.add(book);
notifyObservers(); // 数据变化 → 通知所有 View
}
private void notifyObservers() {
for (BookView view : observers) {
view.update(books); // View 自动刷新
}
}
}
好处:Model 完全不知道有哪些具体的 View 存在,新增 View(如移动端)无需修改 Model。
6.5 MVC 与三层架构的关系(易混淆!)
这是一个常见误解:MVC 和三层架构并不是同一件事。
| MVC 组件 | 对应三层架构的位置 |
|---|---|
| View | 展示层 |
| Controller | 展示层 |
| Model | 业务逻辑层 + 数据访问层 |
- 分层:关注纵向职责划分(谁管展示/谁管逻辑/谁管数据)
- MVC:关注交互逻辑分离(用户输入→处理→展示的流程)
- 实践中:MVC 通常嵌入在三层架构的展示层内部
1
2
3
4
5
6
三层架构视角:
展示层 [Controller + View] ← MVC 活跃在这里
↓
业务逻辑层 [Service = Model 的一部分]
↓
数据访问层 [DAO = Model 的一部分]
6.6 Spring MVC 代码示例
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
33
34
35
36
37
38
39
40
41
42
43
// ===== Controller 层 =====
// 职责:接收用户 HTTP 请求,调用 Service,选择 View
@Controller
public class BookController {
@Autowired
private BookService bookService; // Model 的一部分
// GET 请求:展示图书列表
@GetMapping("/books")
public String listBooks(Model model) {
List<Book> books = bookService.findAll();
model.addAttribute("books", books); // 将数据传给 View
return "bookList"; // 选择 View 模板(bookList.html)
}
// POST 请求:添加新书
@PostMapping("/books")
public String addBook(@RequestBody Book book) {
bookService.save(book); // 更新 Model
return "redirect:/books"; // 重定向到列表页(刷新 View)
}
}
// ===== Service 层(Model 的核心部分)=====
@Service
public class BookService {
@Autowired
private BookDao bookDao;
public List<Book> findAll() {
return bookDao.selectAll();
}
public void save(Book book) {
// 业务规则:书名不能为空
if (book.getTitle() == null || book.getTitle().isEmpty()) {
throw new IllegalArgumentException("书名不能为空");
}
bookDao.insert(book);
}
}
7. 管道-过滤器风格(Pipe-and-Filter)
7.1 核心思想
数据像流水一样通过一系列处理单元(过滤器),每个过滤器只关注自己的输入和输出,彼此完全独立。
1
2
3
数据源 → [Filter A: 解析] → [Filter B: 转换] → [Filter C: 验证] → 数据汇
每个过滤器
独立无状态 数据通过管道单向流动 可组合、可并行
7.2 关键特性
- 过滤器独立无状态:每个 Filter 不保留处理历史,可独立测试
- 数据单向流动:无反向依赖
- 极松耦合:Filter 之间完全不知道彼此的存在
7.3 代码示例
Java Stream API(管道-过滤器的典型实现):
1
2
3
4
5
6
// 每个操作是一个独立的过滤器,通过流(管道)连接
List<String> expensiveBookTitles = books.stream()
.filter(b -> b.getPrice() > 50) // Filter 1: 价格筛选
.sorted(Comparator.comparing(Book::getPrice)) // Filter 2: 排序
.map(Book::getTitle) // Filter 3: 数据转换
.collect(Collectors.toList()); // 收集结果
Unix 管道(管道-过滤器的经典应用):
1
2
3
4
5
cat access.log \
| grep "POST /api/orders" # Filter 1: 筛选订单请求
| awk '{print $1}' # Filter 2: 提取 IP 地址
| sort # Filter 3: 排序
| uniq -c # Filter 4: 统计去重
编译器也是管道-过滤器:词法分析 → 语法分析 → 语义分析 → 代码生成,每个阶段是一个过滤器。
7.4 局限性
- 难以支持交互:数据是单向流动的,不适合需要用户随时介入的场景
- 不适合需要共享状态的场景:过滤器之间无法共享数据(这既是优点也是限制)
8. 客户端-服务器 + REST 风格
8.1 REST 核心约束(Fielding 2000)
REST(Representational State Transfer)不是一种技术,而是一种架构风格约束:
| 约束 | 含义 |
|---|---|
| 无状态 | 每次请求包含所有信息,服务器不保存会话 |
| 统一接口 | HTTP 动词(GET/POST/PUT/DELETE)+ URI 资源 |
| 分层系统 | 客户端不知道是否直连服务器(可能有代理/缓存层) |
| 资源标识 | 一切皆资源,用 URI 唯一标识 |
8.2 RESTful API 设计示例(在线书店)
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
33
@RestController
@RequestMapping("/api/books")
public class BookController {
@Autowired
private BookService bookService;
// GET /api/books?keyword=java&page=1 → 搜索图书
@GetMapping
public Page<Book> search(@RequestParam String keyword,
@RequestParam(defaultValue = "1") int page) {
return bookService.search(keyword, page);
}
// GET /api/books/{id} → 图书详情
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return bookService.findById(id);
}
// POST /api/orders → 创建订单
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED)
public Order createOrder(@RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
// DELETE /api/cart/items/{id} → 删除购物车商品
@DeleteMapping("/cart/items/{id}")
public void removeFromCart(@PathVariable Long id) {
cartService.removeItem(id);
}
}
9. 微服务风格(Microservices)
9.1 核心特征
- 每个服务独立部署、独立数据库(数据自治)
- 通过 REST 或消息队列通信
- 团队按服务边界划分(Conway 定律:系统架构 = 团队沟通结构)
9.2 单体 vs 微服务对比
| 维度 | 单体(Monolith) | 微服务(Microservices) |
|---|---|---|
| 部署 | 整体部署 | 独立部署 |
| 扩展 | 整体扩展(浪费) | 按需扩展(精准) |
| 技术栈 | 统一(耦合技术选型) | 可异构(服务 A 用 Java,B 用 Go) |
| 复杂度 | 代码内(逻辑复杂) | 运维层(网络、服务发现、分布式事务复杂) |
| 适合 | 小团队、早期产品 | 大团队、高流量、组织已成熟 |
9.3 微服务不是银弹(工程权衡)
微服务引入的新问题:
- 分布式事务(跨服务的数据一致性)
- 服务发现(服务 A 怎么找到服务 B?)
- 网络故障(调用链路长,任何一环故障都影响全局)
- 运维复杂度(需要 K8s、服务网格、链路追踪等基础设施)
5 人团队 + 无 K8s 经验 → 微服务是灾难,选单体分层架构。微服务适合的前提是:组织已经大到单体架构限制了独立部署的能力。
10. 体系结构设计过程(七步法)
10.1 整体流程
1
2
1.分析需求 → 2.选择风格 → 3.逻辑设计 → 4.接口设计 → 5.物理设计 → 6.验证设计 → 7.评审改进
↑_________________________发现问题,回退迭代___________________________________|
关键:设计过程是迭代的。步骤 3(逻辑设计)是最核心的步骤,往往需要多轮迭代才能让模块划分合理。
10.2 各步骤详解
步骤 1:分析关键需求和项目约束
体系结构需求由三部分构成:
1
2
3
4
5
6
7
8
9
10
11
体系结构需求
├─ 功能需求(10 个用例)
├─ 非功能需求
│ ├─ 质量属性(可维护性、可扩展性、安全性)
│ ├─ 性能约束(响应时间 < 2s,并发 > 1000)
│ └─ 接口需求(兼容第三方支付、对接 ERP)
└─ 项目约束
├─ 团队(规模、技能)
├─ 预算
├─ 进度
└─ 技术(必须使用 Java + Spring)
重要洞察:非功能需求和项目约束对架构选择的影响往往大于功能需求。一个系统能做什么是功能需求决定的,但用什么方式来做、能做多好,是非功能需求决定的。
步骤 2:选择体系结构风格
| 需求特征 | 推荐风格 | 原因 |
|---|---|---|
| 企业级业务系统、团队分工 | 分层 | 职责分明、成熟框架支持、易于分工 |
| 数据流式处理、转换链 | 管道-过滤器 | 数据逐步变换 |
| 高交互、多视图同步 | MVC | 界面与逻辑分离 |
| 大规模、独立部署、多团队 | 微服务 | 独立开发/部署/扩展 |
| 松耦合、异步通信 | 事件驱动 | 发布/订阅解耦 |
| 小型工具、脚本 | 主程序-子程序 | 简单直接 |
步骤 3:逻辑设计(最核心)
任务:将功能需求分配到各子系统和模块中。
设计原则:
- 高内聚:相关功能放在同一模块
- 低耦合:模块间依赖最小化
- 单一职责:每个模块只做一件事
- 信息隐藏:隐藏实现细节,暴露接口
操作步骤:
- 识别功能域(Sales, Inventory, Member…)
- 为每个功能域创建三层模块(UI, BL, Data)
- 定义模块间的依赖关系
- 检查模块粒度是否合理
步骤 4:接口设计
- 定义模块对外暴露的 API(方法名、参数、返回值、异常)
- 使用 Interface 定义接口,DTO/VO 传递数据,实现解耦
步骤 5:物理设计
- 决定部署拓扑(单机/分布式)
- 数据库选型与部署方案
- 网络、中间件配置
步骤 6:验证设计方案
- 对照需求逐条检查(每个 ASR 都有对应的架构决策吗?)
- 场景走查(Scenario Walkthrough):模拟典型场景,检查架构能否支撑
- 原型验证关键风险点
步骤 7:评审与改进
- 组织架构评审会
- 检查一致性与完整性
- 记录架构决策记录(ADR)
- 输出《架构设计文档》
10.3 常见迭代场景
| 场景 | 回退到哪步 | 原因 |
|---|---|---|
| 接口设计时发现模块职责不清 | 步骤 3 | 重新划分模块边界 |
| 物理部署时发现性能瓶颈 | 步骤 2/3 | 调整风格或拆分模块 |
| 验证时发现安全需求遗漏 | 步骤 1 | 补充需求分析 |
| 评审时团队对设计方案有分歧 | 步骤 3 | 重新讨论模块划分 |
11. 架构重要需求(ASR)
定义
ASR(Architecturally Significant Requirement):影响软件体系结构决策的需求。
并非所有需求都是 ASR。只有那些驱动架构决策的需求才是 ASR。”按钮颜色”不影响架构,”支持 1000 并发”影响架构。
识别 ASR 的标准
以在线书店为例:
| 需求 | 是否 ASR | 理由 |
|---|---|---|
| 支持 1000 并发用户 | ✅ ASR | 驱动缓存/负载均衡决策 |
| 支付信息加密传输 | ✅ ASR | 驱动安全层/HTTPS 决策 |
| 7×24 可用,年可用率 99.9% | ✅ ASR | 驱动冗余部署/故障转移决策 |
| 系统需用 Java 开发 | ✅ ASR | 技术约束,影响全部技术选型 |
| 按书名搜索图书 | ❌ 非 ASR | 功能需求,不影响整体架构 |
| 按钮颜色为蓝色 | ❌ 非 ASR | UI 细节,与架构无关 |
12. 经典架构设计方法:QAW / ADD / ATAM
这三种方法来自 SEI(软件工程研究所),构成一个完整的架构设计-评估闭环:
1
QAW → 产生场景 → ADD → 设计方案 → ATAM → 评估验证 → 反馈迭代
QAW(Quality Attribute Workshop)
- 定位:需求分析阶段
- 目的:收集质量属性场景
- 过程:引导利益相关者头脑风暴,产生质量属性场景(刺激-环境-响应格式)
- 输出:可度量的质量属性需求,如”并发 1000 用户时响应时间 < 2s”
- 解决:”设计目标是什么”
ADD(Attribute-Driven Design)
- 定位:架构设计阶段
- 目的:基于质量属性构建架构
- 核心思想:质量属性驱动设计决策,而非功能列表驱动
- 过程:递归分解系统 → 选择满足质量属性的模式/风格 → 验证权衡
- 解决:”如何根据目标做设计”
ATAM(Architecture Tradeoff Analysis Method)
- 定位:架构评估阶段
- 目的:评估架构方案,发现风险
- 过程:场景映射 → 架构展示 → 识别敏感点和权衡点 → 风险评级
- 解决:”设计方案是否可行”
敏感点:架构中对某个质量属性影响很大的决策点 权衡点:影响多个质量属性,且存在互相制约的决策点(如”加缓存提升性能,但牺牲数据一致性”)
底层循环模型(SWEBOK)
所有架构设计方法都围绕这个循环:
1
2
Analysis(收集 ASR)→ Synthesis(构建候选方案)→ Evaluation(验证方案)
↑_________________________发现问题,循环迭代__________________________|
13. 案例:连锁超市系统——从需求到架构
13.1 需求分析
功能需求(10 个): 商品管理、销售收银、退货、库存盘点、会员管理、促销策略、财务记录、员工管理、日志审计、报表生成
非功能需求(关键 ASR):
| 编号 | 类型 | 描述 |
|---|---|---|
| Security1 | 安全 | 用户身份认证 |
| Security2 | 安全 | 权限分级控制 |
| Security3 | 安全 | 操作日志审计 |
| IC2 | 约束 | 需兼容已有 ERP |
项目约束: Java + Spring Boot,5 人团队,3 个月交付
13.2 选择分层风格的理由
| 考虑因素 | 分析 | 结论 |
|---|---|---|
| 系统类型 | 企业级业务信息系统 | ✅ 适合分层 |
| 团队技术栈 | Java + Spring(天然支持分层) | ✅ 适合分层 |
| 非功能需求 | 安全层可作为独立横切关注点 | ✅ 适合分层 |
| 可维护性 | 功能模块多,需清晰职责划分 | ✅ 适合分层 |
| 项目约束 | 进度紧,分层架构成熟度高 | ✅ 适合分层 |
决策:三层分层架构 + 松散分层策略
13.3 逻辑设计的演化
方案一(有问题):每层只有一个包
1
2
3
presentation(所有 UI 混在一起)
businesslogic(所有业务逻辑混在一起)
data(所有数据访问混在一起)
问题:违反单一职责原则,团队并行开发困难,不同功能的 SQL 耦合在一起。
方案二(改进):三层均按功能域拆分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
presentation/
├── salesui
├── memberui
├── commodityui
├── promotionui
└── userui
businesslogic/
├── salesbl
├── memberbl
├── commoditybl
├── promotionbl
└── userbl
data/
├── salesdata
├── memberdata
├── commoditydata
├── promotiondata
└── userdata
改进:每层约 5 个包,粒度合理,支持团队并行开发。
方案三(最优):接口独立层 + 依赖倒置
这是更成熟的做法,核心改进:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
展示层 (Presentation)
├── Controller
└── View
↓ 依赖
业务逻辑层接口 (Service Interface) ──→ VO(视图对象)
├── IBookService
└── IOrderService
↑ 实现
业务逻辑层实现 (Service Impl) ──→ PO(持久化对象)
├── BookServiceImpl
└── OrderServiceImpl
↓ 依赖
数据访问层接口 (DAO Interface)
├── IBookDao
└── IOrderDao
↑ 实现
数据访问层实现 (DAO Impl)
├── BookDaoImpl
└── OrderDaoImpl
关键改进:
| 改进点 | 说明 |
|---|---|
| 接口独立成层 | Service Interface 和 DAO Interface 作为独立层 |
| 上层依赖接口 | 展示层依赖 Service Interface,不依赖实现(可替换) |
| VO/PO 分离 | VO 用于展示层-逻辑层传递;PO 用于逻辑层-数据层传递 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// VO(View Object):面向展示层,包含展示所需的字段
public class BookVO {
private Long id;
private String title;
private String authorName; // 可能是 Author 表 join 出来的
private String formattedPrice; // "¥ 99.00",格式化后的字符串
}
// PO(Persistent Object):面向数据库,字段与数据库表一一对应
public class BookPO {
private Long id;
private String title;
private Long authorId; // 外键
private BigDecimal price; // 原始精度
}
13.4 跨模块数据依赖问题
销售功能(Sales)的业务逻辑层需要调用:
CommodityBL:查询商品信息MemberBL:计算会员折扣PromotionBL:应用促销策略FinanceBL:记账
这种业务逻辑层内部的横向调用是正常的,关键是通过接口而非直接访问对方的数据层来解耦。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class SalesServiceImpl implements ISalesService {
@Autowired private ICommodityService commodityService; // 依赖接口
@Autowired private IMemberService memberService;
@Autowired private IPromotionService promotionService;
public SaleResult checkout(CheckoutRequest request) {
// 查商品
CommodityVO commodity = commodityService.findById(request.getCommodityId());
// 查会员折扣
double discount = memberService.getDiscount(request.getMemberId());
// 应用促销
double finalPrice = promotionService.apply(commodity.getPrice(), discount);
// ...
}
}
14. 代码练习
练习题:学生选课系统——业务逻辑层设计
背景:设计一个简单的选课系统,支持学生选课、退课、查询课表。
要求:
- 按照方案三(接口独立层 + 依赖倒置)的思路,设计以下层次的关键接口和类:
- 核心服务接口:
ICourseService,IStudentService,IEnrollmentService - 实体/VO 类:
Course,Student,Enrollment - 关键方法(至少 3 个)
- 核心服务接口:
- 实现
EnrollmentServiceImpl.enroll()方法,需要包含以下业务规则:- 检查课程是否存在
- 检查学生是否已经选过该课(不能重复选)
- 检查课程是否还有余量(容量限制)
- 通过检查后,创建 Enrollment 记录,并更新课程已选人数
参考结构(接口设计,供参考,可自行扩展):
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
33
34
35
36
37
38
// 实体类
public class Course {
private Long id;
private String name;
private int capacity; // 课程容量
private int enrolledCount; // 已选人数
// getters/setters...
}
public class Student {
private Long id;
private String name;
private String studentNo;
// getters/setters...
}
public class Enrollment {
private Long id;
private Long courseId;
private Long studentId;
private LocalDateTime enrollTime;
// getters/setters...
}
// 服务接口(需要你补充方法)
public interface IEnrollmentService {
// TODO: 定义选课、退课、查询课表的方法签名
}
// 服务实现(核心,需要你完整实现)
public class EnrollmentServiceImpl implements IEnrollmentService {
private ICourseDao courseDao;
private IStudentDao studentDao;
private IEnrollmentDao enrollmentDao;
// TODO: 实现选课方法,包含上述业务规则
// 提示:按顺序检查每个业务规则,任何一个失败就抛出对应的异常
}
思考问题(不需要写代码,写在注释里即可):
EnrollmentService在调用CourseService还是直接调用CourseDao?为什么?(提示:同层服务之间的调用)- 如果未来需要支持”选课成功后发送邮件通知”,在哪里加这个逻辑?不影响哪些已有代码?
参考答案要点(核心逻辑,非完整代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void enroll(Long studentId, Long courseId) { // 1. 检查课程是否存在 Course course = courseDao.findById(courseId); if (course == null) throw new CourseNotFoundException(...); // 2. 检查是否重复选课 boolean alreadyEnrolled = enrollmentDao.exists(studentId, courseId); if (alreadyEnrolled) throw new DuplicateEnrollmentException(...); // 3. 检查课程余量 if (course.getEnrolledCount() >= course.getCapacity()) { throw new CourseFullException(...); } // 4. 创建选课记录 Enrollment enrollment = new Enrollment(studentId, courseId, LocalDateTime.now()); enrollmentDao.insert(enrollment); // 5. 更新课程已选人数(注意:这里需要考虑并发安全) courseDao.incrementEnrolledCount(courseId); }思考题答案:
EnrollmentService直接调用CourseDao(而非CourseService)是合理的——避免循环依赖,且这里只是简单的数据查询。”发邮件”这个逻辑加在EnrollmentServiceImpl.enroll()的最后,或者更好的做法是通过事件(Observer 模式/Spring ApplicationEvent)解耦,这样EnrollmentService不用知道发邮件的事,完全符合开闭原则。