架构设计原则和规约:写给一线开发的「进阶武功心法」
当你不再满足只写需求,而开始关心「系统是不是优雅、可扩展、抗高并发」,这章就是给你的。
很多人学架构,第一反应是各种流行词:微服务、DDD、中台、云原生……
但真正在复杂系统里站得住脚的东西,其实是那一套「看起来老生常谈」的设计原则和规约。
本章想帮你做到:
- 把零散听过的 SOLID、组合优于继承、DRY、KISS 这些原则串成一套完整「心法」
- 知道它们在高并发 / 高可用系统里具体怎么落地,而不是只背定义
- 面试被问「说说你项目里的架构设计原则」时,有案例、有说服力
一、从 SOLID 开始:六大门派的「入门内功」
先把名字捋清楚,SOLID 这五个字母,对应的是一整套经典设计原则:
- S – Single Responsibility:单一职责原则
- O – Open/Closed:开闭原则
- L – Liskov Substitution:里氏替换原则
- I – Interface Segregation:接口隔离原则
- D – Dependency Inversion:依赖倒置原则
这里面最常被提起、也最容易直接用到的是前两个:
- 单一职责:一个类 / 模块只干一类事儿,改它的理由应该只有一种
- 开闭原则:对扩展开放,对修改关闭
1. 开闭原则:新需求优先用「扩展」,少动老代码
先看一个很常见的反例。
假设你在做一个课程平台,一开始只有 A、B、C 三门课,你可能这么设计接口:
interface CourseService {
void sellCourseA(...);
void sellCourseB(...);
void sellCourseC(...);
}
后来要加 D、E、F 课程,每次都得:
- 改接口、加方法
- 改实现类、加逻辑
- 回归一大片代码
每加一个新课,都要「修改」原来的设计,这就是典型违反开闭原则的设计。
更好的方式是抽象出「课程」这个概念,让新增能力走「扩展」路径:
interface Course {
BigDecimal getPrice();
void sell();
}
class NormalCourse implements Course { ... }
class ArticleCourse implements Course { ... }
class FreeCourse implements Course { ... }
class LiveCourse implements Course { ... }
当要加新课程形态(比如训练营 / 直播课)时:
- 只需要加一个实现类,复用既有抽象
- 不需要反复改原有接口和调用方
在大型项目里,这种「通过抽象 + 多态来扩展」的设计,是你能撑住长期演进的关键。
也可以反过来检查自己代码:
- 如果每加一类新玩法,都要改原来 N 多类 / N 多 if-else,就该考虑是不是该引入开闭原则了。
2. 单一职责:一生只爱一类事儿的类
再看另一个经典坑:一个「大而全」的类。
比如:
class Course {
// 展示课程详情
void getDetail();
// 计算优惠价
void calculatePrice();
// 下单
void buy();
// 获取数据库连接
Connection getConnection();
}
所有东西都往 Course 里堆:
- UI 展示逻辑
- 价格计算规则
- 下单流程
- 数据库访问细节
结果是:
- 任何一个需求变更多半都得改这个类
- 一改就牵一大块,很难局部测试 / 回归
按单一职责拆一下,结构会清晰很多:
Course:只负责承载课程自身信息(名称、描述、基础价格等)PriceEngine:负责所有价格 / 优惠计算OrderCenter:负责下单流程CourseDao:负责数据库访问 / 持久化
这样带来的好处是:
- 新增优惠规则 → 改
PriceEngine - 新增支付方式 → 改
OrderCenter - 换存储介质 → 改
CourseDao
而不需要动来动去改一个「上帝类」。
你会发现:单一职责 + 开闭原则,经常是成对出现的:
- 先用单一职责把不同关注点拆开
- 再用开闭原则为每块提供「可扩展、不乱改」的骨架
二、组合优于继承:别让继承树把自己埋了
很多人刚学 OOP 时会疯狂用继承:
- 看到相似点就抽父类
- 层层继承,结果搞出一棵谁也不敢动的大树
继承当然有用,但问题在于:
- 父类一改,子类全受影响
- 一旦设计错了抽象,后期很难调整
- 违背开闭原则:父类的小改动会「穿透」到所有子类
相比之下,组合 / 聚合更灵活:
- 一个类「拥有」另一个类,而不是「是」另一个类
- 通过组合不同组件,拼出所需行为
- 改动影响范围更小,也更好测试和替换
1. 组合模式的小例子:体系课程和子课程
以课程为例:
- 单门课程:
LeafCourse - 课程体系(包含多门子课):
CoursePackage
可以用一个简单的组合结构:
interface CourseComponent {
void add(CourseComponent c);
void remove(CourseComponent c);
void show();
}
class LeafCourse implements CourseComponent { ... }
class CoursePackage implements CourseComponent {
List<CourseComponent> children;
...
}
这样:
CoursePackage里既可以放LeafCourse,也可以放另一个CoursePackage- 客户端只和
CourseComponent打交道,不关心是「叶子」还是「组合」
这就是典型的组合模式。
组合相比大继承树的好处:
- 结构简单:只有「叶子 + 组合 + 抽象」三类角色
- 扩展方便:加新类型只需实现接口,不必改原有结构
- 更符合开闭:加类而不是改类
你可以在 JDK 和各种开源框架源码里,看到大量类似用法,比如:
- IO 流的装饰器(
InputStream+ 一堆包装类) - Spring 里各种
BeanPostProcessor列表
一个实用建议:
- 凡是你一写就想继承的地方,先停 5 秒,问自己:用组合能不能更好?
三、高并发视角下的设计:局部并发 + 服务拆分 + 高可用手段
设计原则不是只停在类图里,高并发场景下,同样有一套「架构级别」的规约需要你掌握。
1. 局部并发:把能并行的步骤并起来
以电商「下单」为例,真实的一条下单链路可能会做:
- 获取商品信息
- 校验地址 / 用户状态
- 计算优惠
- 锁定库存
- 生成订单 / 快照
- 生成支付链接 / 自动扣款
- 刷新营销、汇总账务……
如果你傻乎乎把这些都串行执行:
- 每一步都等前一步完成,整体 RT 会被拉得很长
- 高并发下吞吐量会急剧下降
更合理的做法是把链路切成几段,在每段内做局部并发:
- 第一段:并行获取商品信息 / 地址 / 账户状态 → 做第一层校验
- 第二段:并行计算优惠 / 锁库存 / 生成支付链接 → 再做一次校验
- 第三段:写订单 / 快照 / 扣减库存 → 最终落地
技术层面可以用:
- JDK 的
Future/CompletableFuture/ 线程池 - 响应式框架(Reactor/RxJava)
- Orchestration / 编排服务
只要把中间检查点设计好,就能在不牺牲正确性的前提下,把 RT 压下来。
你可以记一句话:
高并发系统,本质是 QPS 和 RT 的博弈,局部并发就是在不牺牲正确性的前提下,压缩 RT。
2. 服务拆分:从压力模型和主链路出发
在微服务拆分上,除了 DDD/领域模型,还有两个很实用的维度:
-
压力模型:
- 高频匀速流量:商品详情、搜索、优惠计算
- 低频瞬时流量:秒杀、批量上架、批量改价
- 对高并发场景,单独拆服务、单独配资源(缓存、MQ、限流)
-
主链路规划:
- 从「用户完成一次交易」视角画出:
- 搜索 → 详情 → 加购 → 下单 → 支付 → 收货
- 主链路上的服务优先保证可用性与弹性扩缩
- 非主链路服务(如推荐、埋点、部分营销透出)要设计好降级策略
- 从「用户完成一次交易」视角画出:
大厂真实做法往往是:
- 先从业务和压力角度,抽出主链路 + 高频场景做服务隔离
- 再在每个大领域内用 DDD 做更细的拆分
你可以在自己项目里练习:
- 画出一条真实的用户主链路
- 标出哪些服务 / 操作是「没它就不能成交」的
- 再看它们是否已经单独拆分、是否有自己的稳定性策略
3. 高可用五件套:降级、限流、弹性、切流、回滚
高可用设计里,有几件「标配武器」你必须熟:
-
降级:
- 主链路扛不住时,用「次优结果」把用户打发走
- 如:详情页营销价计算失败,只展示原价 +「请在结算页查看最终优惠」
-
限流:
- 网关层(Nginx / API Gateway)做粗粒度限流:QPS/IP/连接数等
- 业务层做细粒度限流 + 降级:针对某接口、某用户群、某业务场景
-
弹性扩缩容:
- K8s、Docker 等容器编排实现按负载自动扩缩
- 主链路服务在大促时优先扩容,下游服务也要「谈都谈」,跟着扩
-
流量切换:
- 多机房、多活集群之间的流量迁移
- 入口层(DNS)、网关层、客户端(直连配置)都可以是切换点
-
回滚 & 版本控制:
- 不只是代码要可回滚,配置、CI/CD Pipeline 也要有版本和回退方案
- 真的出事时,最简单、最可靠的动作往往就是「回滚到上一个稳定版本」
记一个高可用圈子里的「潜规则」:
万物皆可回滚。不能回滚的,就是潜在事故源。
四、让架构简单一点:DRY、KISS 和 YAGNI
做着做着架构,很容易越搞越复杂。
为了不把自己先绕晕,有三条简单但很有用的「自我约束」:
1. DRY:Don't Repeat Yourself
不要重复自己,典型落地有三种:
-
代码级复用:
- 公共逻辑抽成组件 / SDK / 工具包,而不是到处 copy
- 比如统一的审计日志、统一的返回模型、统一的异常封装
-
业务级复用:
- 识别各业务线重复实现的能力,比如:
- 用户画像
- 营销引擎
- 订单号生成 / 全局 ID
- 尝试沉淀成平台 / 中台服务
- 识别各业务线重复实现的能力,比如:
-
思路级复用:
- 一次解决了某类问题(比如「高并发下单」),
把过程和方案沉淀下来,后面遇到类似问题有模板可用
- 一次解决了某类问题(比如「高并发下单」),
2. KISS:Keep It Simple and Stupid
简单,可维护 > 炫技。
在框架 / 组件设计上尤其重要:
-
无感知 / 低侵入:
- 业务不应该被绑死在某个框架实现上
- 像 Spring Cloud 这种:底层可以换 Netflix / Alibaba 组件,但业务几乎不感知
-
接入成本低:
- 理想状态是「加一个依赖 + 几行配置 + 一两个注解就能用」
- 如果一个组件要接好几天文档才能集成,后面没人会愿意用
-
代码简洁:
- 方法不要太长,一个方法如果要写超过 100 行,很可能是拆分不合理
- 多用小函数 + 清晰命名,而不是一坨长逻辑
3. YAGNI:You Aren't Gonna Need It
你大概率用不到那些你现在想象中的「未来高级能力」。
典型的过度设计包括:
- 系统还在单体阶段,就开始上分布式事务 / 工作流引擎 / 规则引擎
- 业务量刚起步,就引入一堆复杂中间件,只是为了「架构图好看」
- 为了简历好看,强行用上图数据库、消息总线、NoSQL 集群……
更建议的做法是:
- 先预留好演进路线(知道未来可能会变成什么样)
- 但在没到那个量级之前,使用最简单够用的方案
一句丑话:
面向简历编程,只会让你以后面向自己背锅。
五、组件交互与行为:CQS 和关注点分离
在更高一层的设计里,还有两个理念值得你养成习惯:
1. CQS:命令和查询分离
Command Query Separation,说白了:
- 查询(Query):只读,不修改状态
- 命令(Command):修改状态,不返回复杂结果(通常只返回成功 / 失败)
在高并发系统里,这会进一步演化成:
- 读写分离:
- 写库负责事务性修改
- 读库(甚至搜索引擎 / 缓存)负责高并发查询
- 通过 Binlog / Canal / 同步任务实现最终一致
这样设计的好处:
- 读库可以横向扩展(读多写少的典型互联网场景)
- 写路径可以更专注事务和一致性控制
CQS 的思想其实很简单,但如果你从现在开始在接口设计时就刻意区分:
- 哪些是「只读」查询
- 哪些是「只写 / 改」命令
你的系统可维护性和扩展空间会明显好很多。
2. 关注点分离:横向拆业务,纵向拆结构
关注点分离(Separation of Concerns)可以从三个维度理解:
-
横向业务拆分:
- 不同业务领域拆成不同服务 / 模块(商品、订单、用户、营销……)
-
纵向结构拆分:
- UI / API / Service / Repository / Infra 等分层
- 每层只关心自己的职责
-
无感知的基础能力:
- AOP:事务、权限、审计日志等
- 代理:代理数据源、代理 RPC 客户端
- 框架启动器:Spring Boot Starter 按约定自动装配
核心目标是:
- 业务代码里专注业务
- 横切能力(日志、监控、权限、事务等)由框架 / 基础设施透出
- 该抽象的抽象,该配置的配置,不要混在一起写
小结:把原则练进习惯,而不是背在嘴上
这一章串了一圈:
- 微观层面:SOLID、组合优于继承、DRY、KISS、YAGNI
- 架构层面:局部并发、服务拆分、高可用五件套、CQS、关注点分离
真正拉开高级工程师 / 架构师差距的,不是「是否听说过这些名词」,而是:
- 你在设计 / 改代码时,会不会下意识地用这些原则检查自己
- 你在项目复盘时,会不会问:
- 这里是不是该用开闭原则?
- 这里是不是该抽出一个组件?
- 这里是不是过度设计了?
当这些原则不再只是 PPT 上的 bullet point,而是你写代码和做设计时的「肌肉记忆」,
你就已经在通往架构师的路上,走出了一大步。下一步,就是在真实的项目里,多练、多犯错、多复盘,把这些心法一点点练实。+