架构原则
软件架构是系统的基石,决定了系统的可维护性、可扩展性和长期演进能力。本章介绍软件架构的核心原则和设计方法。
什么是软件架构
软件架构是系统的高层结构,定义了:
- 组件划分:系统由哪些模块组成,各模块的职责边界
- 交互关系:组件之间如何通信和协作
- 设计决策:关键技术选型和约束条件
- 质量属性:如何满足性能、安全、可维护性等非功能需求
text
┌─────────────────────────────────────────────────────────┐
│ 软件架构层次 │
├─────────────────────────────────────────────────────────┤
│ 业务架构 → 业务流程、领域模型、组织结构 │
│ 应用架构 → 服务划分、接口设计、数据流转 │
│ 技术架构 → 技术选型、中间件、基础设施 │
│ 数据架构 → 数据模型、存储方案、数据治理 │
│ 部署架构 → 部署拓扑、容灾方案、运维体系 │
└─────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
架构的核心价值在于:用合理的成本,构建满足当前需求且易于演进的系统。
核心设计原则
SOLID 原则
SOLID 是面向对象设计的五大原则,同样适用于架构层面:
| 原则 | 名称 | 核心思想 |
|---|---|---|
| S | 单一职责原则 (SRP) | 一个类/模块只负责一个职责 |
| O | 开闭原则 (OCP) | 对扩展开放,对修改关闭 |
| L | 里氏替换原则 (LSP) | 子类可以替换父类而不影响程序正确性 |
| I | 接口隔离原则 (ISP) | 不应强迫依赖不使用的接口 |
| D | 依赖倒置原则 (DIP) | 依赖抽象而非具体实现 |
单一职责原则应用示例:
typescript
// 违反 SRP:一个类处理多个职责
class UserService {
createUser() { /* 用户逻辑 */ }
sendEmail() { /* 邮件逻辑 */ }
generateReport() { /* 报表逻辑 */ }
}
// 遵循 SRP:职责分离
class UserService {
createUser() { /* 用户逻辑 */ }
}
class EmailService {
sendEmail() { /* 邮件逻辑 */ }
}
class ReportService {
generateReport() { /* 报表逻辑 */ }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
依赖倒置原则应用示例:
typescript
// 违反 DIP:高层模块依赖低层实现
class OrderService {
private mysqlRepository = new MySQLRepository();
saveOrder(order: Order) {
this.mysqlRepository.save(order);
}
}
// 遵循 DIP:依赖抽象接口
interface Repository {
save(entity: Entity): void;
}
class OrderService {
constructor(private repository: Repository) {}
saveOrder(order: Order) {
this.repository.save(order);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DRY 原则
Don't Repeat Yourself - 不要重复自己。系统中的每个知识点应该有单一、明确的表示。
重复的类型:
- 代码重复:相同的代码逻辑出现在多处
- 数据重复:相同的数据存储在多个地方
- 逻辑重复:相同的业务规则在多处实现
- 文档重复:相同的信息在多处描述
消除重复的策略:
typescript
// 重复代码
function validateEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validateUserEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// 提取公共逻辑
class Validator {
private static EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
static isValidEmail(email: string): boolean {
return this.EMAIL_REGEX.test(email);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KISS 原则
Keep It Simple, Stupid - 保持简单。简单性是可靠性的前提。
简单性的价值:
- 降低理解和维护成本
- 减少出错的可能性
- 提高开发效率
- 便于团队协作
过度设计的信号:
- 为未来可能不会发生的需求预先设计
- 过度抽象导致理解困难
- 引入不必要的复杂性
- 配置项多于实际需求
保持简单的方法:
typescript
// 过度设计
interface IUserFactory {
create(): IUser;
}
interface IUserBuilder {
setName(name: string): IUserBuilder;
setEmail(email: string): IUserBuilder;
build(): IUser;
}
class UserDirector {
constructor(private builder: IUserBuilder) {}
constructDefaultUser(): IUser {
return this.builder.setName('default').setEmail('default@example.com').build();
}
}
// 简单直接
class User {
constructor(
public name: string,
public email: string
) {}
}
const user = new User('John', 'john@example.com');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
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
其他重要原则
YAGNI (You Aren't Gonna Need It)
不要实现当前不需要的功能。等到真正需要时再实现,可以避免:
- 浪费开发时间
- 增加维护负担
- 引入不必要的复杂性
高内聚低耦合
- 高内聚:模块内部元素紧密相关,共同完成单一功能
- 低耦合:模块之间依赖关系简单,修改一个模块不影响其他模块
text
高内聚示例:
┌─────────────────────┐
│ OrderModule │
│ ┌───────────────┐ │
│ │ OrderService │ │ ← 订单相关逻辑集中
│ │ OrderRepository│ │
│ │ OrderValidator│ │
│ └───────────────┘ │
└─────────────────────┘
低耦合示例:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Module A │────→│ Interface│←────│ Module B │
└──────────┘ └──────────┘ └──────────┘
(抽象层解耦)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
架构决策方法
架构决策记录 (ADR)
使用 ADR (Architecture Decision Record) 记录重要的架构决策:
markdown
# ADR-001: 使用 PostgreSQL 作为主数据库
## 状态
已采纳
## 背景
系统需要存储用户数据和订单数据,要求数据一致性和复杂查询能力。
## 决策
选择 PostgreSQL 作为主数据库。
## 理由
1. 支持 ACID 事务,保证数据一致性
2. 支持 JSON 类型,兼顾灵活性
3. 团队有丰富的 PostgreSQL 经验
4. 开源免费,社区活跃
## 后果
- 需要专门的 DBA 进行运维
- 水平扩展相对复杂
- 需要处理连接池管理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
决策框架
1. 明确问题
- 当前面临的具体问题是什么?
- 问题的紧迫程度如何?
- 不解决会有什么影响?
2. 收集选项
- 有哪些可行的解决方案?
- 每个方案的优缺点是什么?
- 行业内最佳实践是什么?
3. 评估权衡
text
评估维度:
┌─────────────┬────────┬────────┬────────┐
│ 维度 │ 方案A │ 方案B │ 方案C │
├─────────────┼────────┼────────┼────────┤
│ 开发成本 │ 中 │ 低 │ 高 │
│ 运维成本 │ 低 │ 高 │ 中 │
│ 性能表现 │ 高 │ 中 │ 高 │
│ 可扩展性 │ 高 │ 低 │ 高 │
│ 团队能力匹配 │ 高 │ 中 │ 低 │
└─────────────┴────────┴────────┴────────┘1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
4. 做出决策
- 选择综合最优的方案
- 记录决策理由
- 制定实施计划
5. 验证和调整
- 通过原型验证关键假设
- 收集反馈,必要时调整
架构演进策略
渐进式演进
避免大规模重写,采用渐进式演进策略:
text
演进路径:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 单体应用 │ → │ 模块化 │ → │ 服务化 │ → │ 微服务 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
↓ ↓ ↓ ↓
快速迭代 清晰边界 独立部署 独立扩展1
2
3
4
5
6
7
2
3
4
5
6
7
绞杀者模式
逐步用新系统替换旧系统:
text
阶段1:新功能在新系统实现
┌─────────────────────────────────┐
│ 负载均衡器 │
└───────────────┬─────────────────┘
┌───────┴───────┐
↓ ↓
┌───────────────┐ ┌───────────────┐
│ 旧系统 │ │ 新系统 │
│ (核心功能) │ │ (新功能) │
└───────────────┘ └───────────────┘
阶段2:逐步迁移核心功能
┌─────────────────────────────────┐
│ 负载均衡器 │
└───────────────┬─────────────────┘
┌───────┴───────┐
↓ ↓
┌───────────────┐ ┌───────────────┐
│ 旧系统 │ │ 新系统 │
│ (剩余功能) │ │ (已迁移功能) │
└───────────────┘ └───────────────┘
阶段3:完全替换
┌─────────────────────────────────┐
│ 负载均衡器 │
└───────────────┬─────────────────┘
↓
┌───────────────┐
│ 新系统 │
│ (全部功能) │
└───────────────┘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
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
演进原则
1. 小步快跑
- 每次改动控制在可验证范围内
- 频繁发布,快速反馈
- 出问题可快速回滚
2. 向后兼容
- 新版本兼容旧版本
- 提供迁移路径和工具
- 设置合理的过渡期
3. 监控先行
- 演进前建立监控体系
- 关键指标持续跟踪
- 异常情况及时告警
4. 数据优先
- 数据迁移是最复杂的部分
- 保证数据一致性
- 做好数据备份和恢复方案
架构评审清单
在做出架构决策或进行架构评审时,可以使用以下清单:
功能需求
- [ ] 是否满足所有功能需求?
- [ ] 是否考虑了边界情况?
- [ ] 是否有明确的错误处理策略?
非功能需求
- [ ] 性能:是否满足性能要求?
- [ ] 可扩展性:能否应对业务增长?
- [ ] 可用性:是否有容错和恢复机制?
- [ ] 安全性:是否考虑了安全威胁?
可维护性
- [ ] 架构是否清晰易懂?
- [ ] 是否遵循团队编码规范?
- [ ] 是否有完善的文档?
演进能力
- [ ] 是否易于添加新功能?
- [ ] 是否易于替换组件?
- [ ] 是否有清晰的演进路径?
成本效益
- [ ] 开发成本是否合理?
- [ ] 运维成本是否可控?
- [ ] 是否充分利用现有资源?
小结
架构原则不是教条,而是指导思想的集合。在实际项目中:
- 理解原则背后的目的,而非机械套用
- 根据实际情况权衡取舍,没有放之四海而皆准的方案
- 保持简单,避免过度设计
- 记录决策,便于团队理解和后续演进
- 持续演进,架构是动态的,需要随业务发展而调整
好的架构是演出来的,不是设计出来的。从简单开始,根据实际需求逐步演进,才是务实的做法。