Post

体系结构风格与设计过程

体系结构风格与设计过程

前置知识:架构三要素(组件、连接器、约束)、设计分层、技术债务与 ADR

1. 体系结构风格:是什么,为什么需要

定义

体系结构风格(Architectural Style) 是一组设计决策的模板,规定了三件事:

1
风格 = 组件词汇 + 连接器词汇 + 拓扑约束
  • 组件词汇:这种风格里有哪些”构建块”(如层、过滤器、服务)
  • 连接器词汇:组件之间怎么交互(如过程调用、管道、事件总线)
  • 拓扑约束:组件能怎么连(如”分层只能调用直接下层”)

为什么需要风格?

目的解释
经验复用经过工业验证的设计方案,不用从零摸索
通用词汇说”三层架构”,团队所有人立刻理解意图
质量属性权衡明确每种风格有已知的优劣(如分层牺牲性能换可维护性)

关键洞察:风格不是”编程规范”,是系统级的组织原则。选错风格,代码再整洁也救不了架构。


2. 风格 vs 设计模式:粒度之别

这是常见的混淆点,必须厘清:

维度体系结构风格(Style)设计模式(Pattern)
作用范围系统全局组织方式局部重复问题的解决方案
粒度定义系统大部分的结构应用于系统中单个元素
示例分层、微服务、管道-过滤器观察者、工厂、代理
能否共存一个系统通常选一种主风格多个模式可以同时存在

案例:在线书店中风格与模式共存

1
2
3
4
主风格:三层分层架构(全局)
  └─ 展示层内部:MVC 模式
  └─ Model 通知 View:Observer 模式
  └─ 数据访问层:DAO 模式

3. 主要风格一览与适用场景

SWEBOK 将架构风格分为六大类:

#类别包含风格
1General Structures分层、管道-过滤器、黑板
2Distributed Systems客户端-服务器、n-tier、REST微服务
3Method-Driven面向对象、事件驱动、数据流
4User-Computer InteractionMVC、PAC
5Adaptive Systems微内核、反射
6Virtual Machines解释器、规则引擎

各风格快速对比

风格组件连接器典型场景核心优势核心劣势
主程序-子程序主程序、子程序过程调用小脚本、传统系统结构清晰、易调试耦合高、扩展性差
分层过程调用企业信息系统可维护、可测试性能开销、级联修改
面向对象对象/类方法调用现代软件框架封装、继承、多态需良好设计
管道-过滤器过滤器管道(数据流)编译器、数据处理极松耦合、可并行难以交互
MVCModel/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/MS2OOPFES
数据表示变更(索引→完整字符串)改所有模块仅改内部实现改过滤器链调整事件策略
算法变更(按需生成位移)困难(数据耦合)中等改过滤器即可中等
功能扩展(过滤噪声词)直接修改共享数据中等新增过滤器调整监听器

核心结论:没有最好的风格,只有最适合需求的风格。

  • 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:逻辑设计(最核心)

任务:将功能需求分配到各子系统和模块中。

设计原则:

  • 高内聚:相关功能放在同一模块
  • 低耦合:模块间依赖最小化
  • 单一职责:每个模块只做一件事
  • 信息隐藏:隐藏实现细节,暴露接口

操作步骤:

  1. 识别功能域(Sales, Inventory, Member…)
  2. 为每个功能域创建三层模块(UI, BL, Data)
  3. 定义模块间的依赖关系
  4. 检查模块粒度是否合理

步骤 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功能需求,不影响整体架构
按钮颜色为蓝色❌ 非 ASRUI 细节,与架构无关

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. 代码练习

练习题:学生选课系统——业务逻辑层设计

背景:设计一个简单的选课系统,支持学生选课、退课、查询课表。

要求

  1. 按照方案三(接口独立层 + 依赖倒置)的思路,设计以下层次的关键接口和类:
    • 核心服务接口:ICourseService, IStudentService, IEnrollmentService
    • 实体/VO 类:Course, Student, Enrollment
    • 关键方法(至少 3 个)
  2. 实现 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 不用知道发邮件的事,完全符合开闭原则。

This post is licensed under CC BY 4.0 by the author.