系统设计——系统设计师与软件设计师的分工,以及自顶向下的局限

一句话

系统设计师关注「整个系统怎么构成和协同工作」,软件设计师关注「软件本身怎么设计和实现」。设计可以自顶向下,实现通常自底向上,验证则逐层向上扩展。


一、系统设计师 vs 软件设计师

核心区别

1
2
3
4
5
6
7
系统设计师

系统架构、硬件、软件、网络、部署、安全、性能

软件设计师

软件架构、模块划分、接口、算法、代码实现

设计范围

系统设计师(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
2
Client.exe: UI模块 / 扫描控制模块 / 更新模块 / 配置模块 / 通信模块
Driver.sys: 文件监控模块 / 进程监控模块 / 内存扫描模块 / IOCTL模块

第三步:详细设计——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
2
业务:订单 / 用户 / 支付
基础设施:日志 / 线程池 / 配置 / 网络 / 序列化 / 异常处理

它们很难从业务需求直接推导,所以现代架构通常会提前留位置:

1
2
3
4
Application
├── Business
├── Infrastructure(Logging / ThreadPool / Config / Database)
└── Framework

五、那架构师到底怎么解决?

不是靠「提前猜中所有模块」。优秀架构设计不是预测全部,而是:

设计允许变化的结构。

错误:

1
OrderService → MySQL API

正确:

1
OrderService → Repository接口 → MySQL

以后 MySQL → Redis → 其他数据库,不用重写业务。

架构师主要决定:

1
2
系统应该存在基础设施层
业务不能直接依赖具体实现

而不是一开始就写 Logger 类、ThreadPool 类、ConfigManager 类——那已经进入详细设计。


六、实际开发更接近「双向设计」

1
需求 → 初步架构 → 编码验证 → 发现问题 → 修改架构 → 再实现

不是「需求 → 完美架构 → 完美代码」,这种不存在。

架构设计不是为了预测所有实现细节,而是为了控制变化范围,让未知需求出现时局部修改,而不是整体推翻。

顶层约束 + 底层探索——演进式架构、增量设计、架构治理。


七、架构不是一次产生的:演化式架构

很多优秀系统是演化出来的。Linux 最开始是简单内核,后来才有驱动系统、网络栈、文件系统、模块加载、安全模块、虚拟化——不是第一天就设计完整的。游戏引擎也是:最开始只有渲染、输入、游戏逻辑,后来才有 ECS、资源管理、任务系统、物理系统、网络系统、脚本系统。


八、先隔离探索再融合的架构演进

直接扩展为什么容易失控?

原项目 Core 里直接加入 CLI,短期可以运行,长期 Core 里挂满 CLI/GUI/Test/Script 需求,最后核心库变成「万能大杂烩」。

独立探索其实是一种架构验证

1
2
3
Experiment
├── libcore.a
└── cli.exe

先验证:API 是否合理?数据结构是否稳定?调用方式是否自然?错误处理是否合理?生命周期是否正确?

发现问题后重新设计接口,比直接污染主工程成本低很多。这实际上叫 Architecture Spike(架构探针)——目的不是做产品,而是验证一个架构选择是否可行。

陷阱与正确方式

不能简单复制一个项目成 OldProject / NewProject 然后两个方向发展(版本分叉)。正确方式:建立边界实验,实验项目只依赖接口,不复制核心

1
2
3
Core API

Experimental CLI(验证接口)

最终融合不是「实验 CLI 复制代码进正式项目」,而是提取结构:

1
2
3
4
Product
├── engine / libengine.a
├── tools / cli.exe
└── apps / gui.exe

核心永远不知道入口存在。

推荐流程

1
2
已有系统 → 设计最小公共接口 → 独立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 内部结构

什么时候才拆独立项目

  1. CLI 是另一个产品:已有 libvideo,现在做 ffmpeg 类似命令工具(消费者可以独立仓库)
  2. 生命周期不同:核心是商业 SDK,CLI 是内部测试工具 → 分开
  3. 部署环境不同:核心是 Windows 静态库,CLI 是 Linux 服务器工具 → 可能独立

AI 闭环时代的变化

以前:人 → 需求 → 架构设计 → 编码。现在:人 → 目标 → AI Agent → 需求模型 → 架构模型 → 代码 → 测试。

但是系统边界设计仍然重要——AI 最大的问题不是写代码,而是它不知道哪些变化应该隔离。例如让 AI「增加 CLI」,它可能直接修改 100 个文件、增加 main(),短期能运行,长期 Core 被 CLI 逻辑污染。所以需要人定义:核心边界、模块职责、扩展方式。

1
2
3
4
5
6
7
8

定义系统边界

AI架构规划

Core Library

CLI / GUI / API(多个入口)

核心稳定,入口不断增加。

「CLI 是不是系统的一部分?」→ 系统设计问题;「CLI 如何调用静态库?」→ 架构设计问题;「main.cpp 怎么解析参数?」→ 详细设计问题。AI 可以自动化后两步,但第一步的边界判断仍然是架构能力。


九、CLI 与静态库:谁驱动谁

CLI 不是静态库的下一层,而是另一个应用

1
2
错误理解:静态库 → CLI → 继续设计
正确理解:静态库项目(API) ← CLI项目(使用者)

类似 Windows API ← Explorer.exe:Explorer 不是 Windows 内部模块,而是调用者。

CLI 不应该从 API 开始,而应该有自己完整的需求链

1
2
CLI想法 → CLI需求分析 → CLI功能点设计 → CLI需要什么能力
→ API能力匹配分析 → 发现API缺失 → 反馈静态库需求 → 扩展静态库 → 重新生成CLI

CLI 是需求的提出者,不是 API 的被动消费者。 如果 CLI 流程只是「API 列表 → 生成 CLI 代码」,它容易退化成「为了调用 API 而写的适配程序」,还会出现 API 不够时 AI 自己 mock 返回值、写假文件的现象。

必须有 API 缺口审查(API Gap Report)

1
2
3
4
需求: 导出报告
需要: ExportReport()
当前: 不存在
状态: 阻塞

发现缺口后,不是 CLI 解决,而是生成「静态库需求变更」,重新进入静态库流程。最终形成闭环:

1
2
3
用户需求 → CLI需求 → API能力分析 → 满足? 
├─ 是 → CLI实现
└─ 否 → API缺失文档 → 静态库需求新增 → 静态库开发流程 → 新API → CLI继续

多个项目共享一个系统设计

1
2
3
4
5
Workspace
├── CoreLibrary(API)
├── CLI(调用 Core API)
├── Test
└── Tools

再往上还有一层:产品系统设计——决定哪些是库、哪些是应用、哪些共享、哪些独立:

1
2
3
4
5
6
产品
├── Core Library
├── CLI Tool
├── GUI Tool
├── Service
└── Plugin

十、设计方向、实现方向、验证方向

软件工程里有一个核心的三角关系:

1
2
3
设计方向:整体 → 系统 → 子系统 → 组件 → 接口        (自顶向下)
实现方向:基础设施 → 组件 → 子系统 → 系统 → 应用 (自底向上)
验证方向:最小单元 → 组件 → 子系统 → 系统 → 整体 (由小到大逐层闭环)

三种顺序不要混

  • A. 架构顺序:谁依赖谁(Registry → System)
  • B. 实现顺序:先写谁(由依赖关系和拓扑排序决定)
  • C. 验证顺序:先让什么跑起来(窗口 → 方块 → 移动 → 碰撞……,不断获得可运行的最小系统)

「我要先开发什么」和「程序运行时先做什么」是两种完全不同的顺序。

开发顺序:Entity → Component → Registry → Movement → Collision → Render
运行顺序:Input → Movement → Collision → Animation → Render

没有设计文档 ≠ 没有设计

老程序员看到需求后可能马上知道:这里需要一个状态机、这里必须解耦、这里要做接口隔离、这个东西不能让上层直接碰——这些东西可能根本没画在图上,但已存在于脑中。

  • 隐式架构设计 + 增量实现 + 持续重构:成熟工程师
  • 既没有显式设计,也没有隐式经验,于是直接堆代码:新手最容易出现的问题

这两者表面上都是「没画设计图」,实际上完全不同。


一句话总结

传统软件工程往往是「架构自顶向下约束,实现自底向上构建,验证由小到大闭环,复杂度上升后再反向调整架构」。


与其他线的关系

  • 与设计起点线:设计起点决定先设计什么(控制面),本线决定谁来设计、设计到哪一层
  • 与设计表示线:系统设计产出边界和依赖方向,设计表示线提供把这些产出画出来的八种视图
  • 与拆解与组织主题:系统设计是拆解与组织原则在「人/职责分工」上的应用——系统设计师管拆,软件设计师管组织
  • 与依赖关系主题:系统设计强调业务不能直接依赖具体实现(依赖接口),是依赖倒置在架构层的体现