02-系统设计
系统设计——系统设计师与软件设计师的分工,以及自顶向下的局限
一句话
系统设计师关注「整个系统怎么构成和协同工作」,软件设计师关注「软件本身怎么设计和实现」。设计可以自顶向下,实现通常自底向上,验证则逐层向上扩展。
一、系统设计师 vs 软件设计师
核心区别
1 | 系统设计师 |
设计范围
系统设计师(System Designer / System Architect) 设计的是完整系统。例如银行交易系统,系统设计师考虑:
- 服务器怎么部署?数据库用什么?是否需要缓存?
- 网络架构如何?如何保证高并发?如何保证安全?如何容灾?
- 用户端、服务器、第三方系统如何通信?是否需要消息队列?硬件资源如何规划?
软件设计师(Software Designer) 设计的是软件内部结构。例如上面的交易服务,软件设计师考虑:
- 类怎么设计?模块怎么拆?接口怎么定义?
- 数据结构怎么选择?算法怎么实现?
- 如何降低耦合?如何提高可测试性?
| 方面 | 系统设计师 | 软件设计师 |
|---|---|---|
| 关注对象 | 整个系统 | 软件程序 |
| 硬件 | 涉及 | 通常不涉及 |
| 网络 | 重点 | 部分涉及 |
| 数据库 | 架构选择 | SQL、ORM 设计 |
| 部署 | 重点 | 辅助 |
| 算法 | 了解 | 深入 |
建筑比喻
- 系统设计师 = 建筑总设计师(决定楼、电、水、结构如何协同)
- 软件设计师 = 室内结构设计师(决定房间布局、管线、装修细节)
实际高级工程师往往两个能力都需要,但系统设计师的视角通常更高、更偏整体架构。
二、单体架构也需要系统设计
单体架构不是不需要系统设计师,而是不一定需要专职系统设计师。
很多人误认为「微服务需要系统设计师,单体应用只需要程序员」,这不对。单体系统同样需要回答:数据放哪、缓存要不要、并发怎么处理、安全边界在哪——只是规模小,一个人可以兼任。
小公司可能一个人全部做:需求分析 → 系统设计 → 软件设计 → 编码 → 部署维护。
二·五、系统设计的触发条件
系统设计的核心不是「有没有拆分」,而是「有没有多个组成部分需要协调」。
当「整体行为」不能由一个程序内部逻辑直接决定、需要多个部分共同完成时,就进入系统设计领域。判断方法看三个维度:
① 多个运行实体
1 | 程序A → 网络 → 程序B |
涉及通信协议、状态同步 → 系统设计增加。
② 多个数据所有者
1 | 用户数据库 / 订单数据库 / 支付数据库 |
涉及数据一致性、权限、生命周期 → 系统设计增加。例如用户支付时「扣钱成功但订单创建失败」怎么办——需要事务、消息队列、重试机制、补偿机制,这些是系统级问题。
③ 多个部署环境
1 | Windows客户端 / Linux服务器 / 云服务器 / 数据库服务器 |
涉及部署、运维、安全 → 系统设计增加。
不是所有拆库或 C/S 都需要专职系统设计师
- 简单 C/S(C++ 客户端 + MySQL):开发人员 = 软件设计人员 = 简单系统设计人员
- 普通拆库(表太大拆库):属于软件架构设计
- 复杂 C/S(游戏:客户端/协议/战斗服/登录服/数据库):必须系统设计,需要决定客户端负责什么、协议如何定义、断线怎么办、如何防作弊、如何扩容
反例:纯算法程序
1 | int quick_sort(int* arr, int n) { ... } |
没有网络、数据库、外部组件、多模块协作,主要是算法设计、软件设计,不是系统设计。
协作层级
1 | 单个函数 → 模块 → 程序 → 多个程序协作 → 多个服务器协作 → 多个业务系统协作 |
越往后,系统设计比例越高。
三、设计层次归属
系统设计、架构设计、详细设计三个层次,处理未知的方式不同:
- 系统设计:提前确定边界、职责、通信、约束。例如客户端 / 协议 / 服务器,不会轻易改变
- 架构设计:提前确定扩展点、依赖方向、模块关系。例如业务 → 抽象接口 → 基础设施,允许变化
- 详细设计:可以随着开发调整。类、函数、数据结构、算法
严格划分:系统设计 = 组成与协作,架构设计 = 内部组织
以杀毒软件 C/S 程序为例,三步分别属于不同层次:
第一步:系统设计——我要做一个完整杀毒系统,需要哪些部分?
1 | 用户 → 杀毒客户端.exe → IOCTL → 内核驱动.sys → 文件系统过滤驱动 → Windows内核 |
决定:为什么需要驱动?客户端负责什么?哪些功能放用户态?哪些放内核态?——涉及不同进程、不同权限层、不同运行环境。
第二步:架构设计——客户端内部怎么组织?
1 | Client.exe: UI模块 / 扫描控制模块 / 更新模块 / 配置模块 / 通信模块 |
第三步:详细设计——MemoryScanner 怎么写?类、函数、数据结构、算法。
为什么容易混淆:「系统」一词的两种含义
- 系统 = 整个产品:浏览器系统(Chrome.exe + 服务器 + 数据库 + 同步服务)→ 系统设计
- 系统 = 软件系统:Chrome.exe 内部(网络模块/渲染模块/JS引擎/UI模块)→ 很多人也叫它系统架构设计
现实中「系统架构师」通常三个层次都会涉及,所以职位名称容易混乱。
建筑比喻(更准确)
- 系统设计 = 城市规划:医院、学校、道路、供水、电网——整个城市怎么运行?
- 架构设计 = 医院建筑:门诊楼、住院楼、手术室、药房、电梯——这个建筑内部怎么组织?
- 详细设计 = 手术室:设备摆放、线路布局、材料规格——具体怎么实现?
ECS 架构 ≠ 系统设计
游戏采用 ECS 不代表必然有专职系统设计师:
- ECS 是软件架构设计(解决游戏对象如何组织、数据如何存储、逻辑如何执行),不是系统设计
- 游戏里的「技能系统 / 战斗系统 / 任务系统」是游戏功能模块,不是架构里的 System Design
- 关键看 ECS 外面还有没有更大的系统边界:单纯游戏程序(ECS + Renderer + Physics)主要是软件架构设计;大型 MMO(客户端 + 服务器集群 + 数据库 + 支付系统 + 反作弊驱动)多个实体协作,才需要系统设计
一个优秀的 ECS 引擎设计者可能是非常厉害的软件架构师,但未必是系统设计师。驱动+客户端、Hypervisor+Loader 这类例子,反而比单纯 ECS 更典型地属于系统设计领域。
四、纯自顶向下会失败
现象: 需求分析只得到「要做的事情需要的部分」,实现时才发现还需要日志、线程池、配置、网络、序列化、异常处理。
例子:做聊天软件。需求分析得到用户登录、发送消息、接收消息、好友列表。架构设计出用户模块、消息模块、好友模块。开始实现,发现发送消息需要:消息队列、网络连接管理、线程池、日志系统、配置系统、缓存、数据库访问层、异常处理。
「只能想到要做的事情需要的部分」这个现象确实存在。
原因: 日志、线程池、配置、网络、序列化、异常处理通常不是业务模块,而是横切关注点(Cross-cutting Concerns):
1 | 业务:订单 / 用户 / 支付 |
它们很难从业务需求直接推导,所以现代架构通常会提前留位置:
1 | Application |
五、那架构师到底怎么解决?
不是靠「提前猜中所有模块」。优秀架构设计不是预测全部,而是:
设计允许变化的结构。
错误:
1 | OrderService → MySQL API |
正确:
1 | OrderService → Repository接口 → MySQL |
以后 MySQL → Redis → 其他数据库,不用重写业务。
架构师主要决定:
1 | 系统应该存在基础设施层 |
而不是一开始就写 Logger 类、ThreadPool 类、ConfigManager 类——那已经进入详细设计。
六、实际开发更接近「双向设计」
1 | 需求 → 初步架构 → 编码验证 → 发现问题 → 修改架构 → 再实现 |
不是「需求 → 完美架构 → 完美代码」,这种不存在。
架构设计不是为了预测所有实现细节,而是为了控制变化范围,让未知需求出现时局部修改,而不是整体推翻。
顶层约束 + 底层探索——演进式架构、增量设计、架构治理。
七、架构不是一次产生的:演化式架构
很多优秀系统是演化出来的。Linux 最开始是简单内核,后来才有驱动系统、网络栈、文件系统、模块加载、安全模块、虚拟化——不是第一天就设计完整的。游戏引擎也是:最开始只有渲染、输入、游戏逻辑,后来才有 ECS、资源管理、任务系统、物理系统、网络系统、脚本系统。
八、先隔离探索再融合的架构演进
直接扩展为什么容易失控?
原项目 Core 里直接加入 CLI,短期可以运行,长期 Core 里挂满 CLI/GUI/Test/Script 需求,最后核心库变成「万能大杂烩」。
独立探索其实是一种架构验证
1 | Experiment |
先验证:API 是否合理?数据结构是否稳定?调用方式是否自然?错误处理是否合理?生命周期是否正确?
发现问题后重新设计接口,比直接污染主工程成本低很多。这实际上叫 Architecture Spike(架构探针)——目的不是做产品,而是验证一个架构选择是否可行。
陷阱与正确方式
不能简单复制一个项目成 OldProject / NewProject 然后两个方向发展(版本分叉)。正确方式:建立边界实验,实验项目只依赖接口,不复制核心:
1 | Core API |
最终融合不是「实验 CLI 复制代码进正式项目」,而是提取结构:
1 | Product |
核心永远不知道入口存在。
推荐流程
1 | 已有系统 → 设计最小公共接口 → 独立CLI+静态库验证 → 发现接口问题 |
不要把 CLI 看成「新增功能」,而应该看成一个新的系统入口(client),它迫使原系统暴露稳定接口。
八·五、AI 闭环下的扩展策略:另起文件夹还是原流程扩展
当 AI 已经能完成「需求 → 分析 → 设计 → 编码 → 编译 → 测试」闭环时,新增一个 CLI + 静态库接口,应该作为独立项目重新设计,还是在原有闭环上增量扩展?
不要另起炉灶重新设计,而是在原有系统设计基础上扩展。但要先判断 CLI 是否属于同一系统边界。
CLI 的角色:给已有能力增加一个新的入口(Adapter)
1 | 原系统 → + CLI入口(而不是:新系统 → + 复制一份逻辑) |
1 | CLI → Public API → Static Library → 核心实现 |
CLI 不应该知道核心内部:错误是 cli 直接调用 class A/B/C,正确是 cli → Public API → Static Library。
为什么不建议重新设计
- 需求分裂:原需求「实现一个计算引擎」+ 新增「增加命令行控制」,重新设计成 Engine V1 + CLI V2,最后变成两个架构、两套逻辑
- 违反抽象原则:核心系统应该是 UI → API → Core,而不是 CLI 直接修改 Core 内部结构
什么时候才拆独立项目
- CLI 是另一个产品:已有 libvideo,现在做 ffmpeg 类似命令工具(消费者可以独立仓库)
- 生命周期不同:核心是商业 SDK,CLI 是内部测试工具 → 分开
- 部署环境不同:核心是 Windows 静态库,CLI 是 Linux 服务器工具 → 可能独立
AI 闭环时代的变化
以前:人 → 需求 → 架构设计 → 编码。现在:人 → 目标 → AI Agent → 需求模型 → 架构模型 → 代码 → 测试。
但是系统边界设计仍然重要——AI 最大的问题不是写代码,而是它不知道哪些变化应该隔离。例如让 AI「增加 CLI」,它可能直接修改 100 个文件、增加 main(),短期能运行,长期 Core 被 CLI 逻辑污染。所以需要人定义:核心边界、模块职责、扩展方式。
1 | 人 |
核心稳定,入口不断增加。
「CLI 是不是系统的一部分?」→ 系统设计问题;「CLI 如何调用静态库?」→ 架构设计问题;「main.cpp 怎么解析参数?」→ 详细设计问题。AI 可以自动化后两步,但第一步的边界判断仍然是架构能力。
九、CLI 与静态库:谁驱动谁
CLI 不是静态库的下一层,而是另一个应用
1 | 错误理解:静态库 → CLI → 继续设计 |
类似 Windows API ← Explorer.exe:Explorer 不是 Windows 内部模块,而是调用者。
CLI 不应该从 API 开始,而应该有自己完整的需求链
1 | CLI想法 → CLI需求分析 → CLI功能点设计 → CLI需要什么能力 |
CLI 是需求的提出者,不是 API 的被动消费者。 如果 CLI 流程只是「API 列表 → 生成 CLI 代码」,它容易退化成「为了调用 API 而写的适配程序」,还会出现 API 不够时 AI 自己 mock 返回值、写假文件的现象。
必须有 API 缺口审查(API Gap Report)
1 | 需求: 导出报告 |
发现缺口后,不是 CLI 解决,而是生成「静态库需求变更」,重新进入静态库流程。最终形成闭环:
1 | 用户需求 → CLI需求 → API能力分析 → 满足? |
多个项目共享一个系统设计
1 | Workspace |
再往上还有一层:产品系统设计——决定哪些是库、哪些是应用、哪些共享、哪些独立:
1 | 产品 |
十、设计方向、实现方向、验证方向
软件工程里有一个核心的三角关系:
1 | 设计方向:整体 → 系统 → 子系统 → 组件 → 接口 (自顶向下) |
三种顺序不要混
- A. 架构顺序:谁依赖谁(Registry → System)
- B. 实现顺序:先写谁(由依赖关系和拓扑排序决定)
- C. 验证顺序:先让什么跑起来(窗口 → 方块 → 移动 → 碰撞……,不断获得可运行的最小系统)
「我要先开发什么」和「程序运行时先做什么」是两种完全不同的顺序。
开发顺序:Entity → Component → Registry → Movement → Collision → Render
运行顺序:Input → Movement → Collision → Animation → Render
没有设计文档 ≠ 没有设计
老程序员看到需求后可能马上知道:这里需要一个状态机、这里必须解耦、这里要做接口隔离、这个东西不能让上层直接碰——这些东西可能根本没画在图上,但已存在于脑中。
- 隐式架构设计 + 增量实现 + 持续重构:成熟工程师
- 既没有显式设计,也没有隐式经验,于是直接堆代码:新手最容易出现的问题
这两者表面上都是「没画设计图」,实际上完全不同。
一句话总结
传统软件工程往往是「架构自顶向下约束,实现自底向上构建,验证由小到大闭环,复杂度上升后再反向调整架构」。
与其他线的关系
- 与设计起点线:设计起点决定先设计什么(控制面),本线决定谁来设计、设计到哪一层
- 与设计表示线:系统设计产出边界和依赖方向,设计表示线提供把这些产出画出来的八种视图
- 与拆解与组织主题:系统设计是拆解与组织原则在「人/职责分工」上的应用——系统设计师管拆,软件设计师管组织
- 与依赖关系主题:系统设计强调业务不能直接依赖具体实现(依赖接口),是依赖倒置在架构层的体现
