Post

软件设计基础与体系结构概念

软件设计基础与体系结构概念

核心问题:软件设计的本质是什么?软件体系结构由哪些要素组成?

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 发展简史

年代里程碑
1972Parnas 提出模块分解原则(信息隐藏)
1992Perry & Wolf 提出经典公式:Elements + Forms + Rationale
1995Shaw 提出部件-连接件-配置模型;Kruchten 提出 4+1 视图
1998SEI(Bass 等)给出迄今最权威的定义
2000IEEE 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 系统
This post is licensed under CC BY 4.0 by the author.