七个分析维度——分析任何程序的完整方法
分析一个程序,不能只画一张「分层架构图」。一个真正完整的工程分析,往往是把结构、调用链、启动链、生命周期、数据流、业务流、状态变化七张图叠起来。这一条线是「分析任何程序」的完整方法论:五个观察维度扩展为七个,用 HTTP 服务器作为贯穿例子(它恰好把七个维度全部暴露得很清楚),并给出最终原则。
一、五个观察维度(后扩展为七个)
把「软件结构」与「软件运行」两套视角合起来,可以整理成:
1 2 3 4 5
| 1. 代码组装位置 2. 调用链 3. 启动流 4. 生命周期 5. 运行流
|
分层回答「代码怎么组织」;调用链回答「谁调用谁」;启动流回答「怎么起来」;生命周期回答「怎么存在」;运行流回答「运行时怎么变化」。
二、HTTP 不只是「通信协议」
更准确地说:
1 2 3
| HTTP ├── 通信协议 └── 应用层请求入口
|
1 2
| 客户端 → HTTP Request → HTTP Server → HTTP Parser → Router → Handler → Service → Domain/Algorithm → Database
|
POST /login 是业务入口,HTTP 是承载这个入口的协议。
启动流 vs 运行流(完全不同)
1 2 3 4 5 6 7 8 9
| 启动流: main → 创建 Server → 创建 HTTP → 创建 Router → 注册 Handler → 绑定端口 → 启动 EventLoop → Server Running (程序如何从不存在进入运行状态)
运行流: HTTP Request → Socket → HTTP Parser → Router → Handler → Service → Domain → Database → Response → HTTP Response (程序运行以后,一次业务请求如何流动)
|
生命周期是另一条线
1 2
| Server:创建 → 初始化 → 启动 → 运行 → 暂停/恢复 → 停止 → 释放 HTTP Connection:Accept → Connected → Read → Parse → Request → Response → Keep-Alive/Close
|
一个程序实际上同时存在多个嵌套生命周期。
代码组装位置回答「谁创建谁」
1
| main → Server(HttpServer:Socket/EventLoop/ConnectionManager;Router;Service;Database)
|
不一定意味着 Server 必须拥有所有东西,而是说明 Server 这个运行子系统的组装边界在哪里。
五维总图
1 2 3
| 【结构】代码组装位置 / 组件关系 → 系统组成 【行为】启动流(初始化)→ 运行流(请求处理)→ 生命周期(状态变化) 【调用】调用链
|
而 HTTP 网络程序恰好把这些全部暴露得非常清楚:HTTP 是通信协议;HTTP Server 是协议入口;Router/Handler 是请求分发入口;Service 是业务入口——「入口」也应该分层理解。
三、Socket 的创建流与协议使用
Socket 中 TCP/UDP 的图示至少包含两条不同的东西:
1. 创建/建立流——生命周期
1 2
| TCP 服务器:socket() → bind() → listen() → accept() → connected → send/recv → close() TCP 客户端:socket() → connect() → connected → send/recv → close()
|
2. 协议使用——运行流
1 2
| 发送:应用数据 → send() → TCP → IP → 网络 接收:网络 → IP → TCP → recv() → 应用数据
|
UDP 完全不同
1
| socket() → bind() → sendto()/recvfrom() → close()
|
UDP 的「创建流」很短,而「协议使用」直接发生在数据报收发过程中。
Socket 学习图重新理解
1 2 3 4 5
| Socket ├── 创建流 socket / bind / listen / accept / connect ├── 协议使用 TCP(send/recv,connection-oriented) │ UDP(sendto/recvfrom,datagram-oriented) └── 生命周期 创建 → 运行 → 关闭
|
Socket API 图并不只是「函数怎么调用」,它展示的是:资源如何创建 → 协议如何进入工作状态 → 数据如何运行 → 资源如何结束。 TCP 与 UDP 最核心的区别可以从生命周期和运行流两个角度同时看,而不只是死记 connect() 有没有。
四、三种「流」:数据流 / 协议流 / 业务流
「数据流」和「运行流」不能混。至少分成三种流:
① 数据流:数据怎么走
1
| 业务数据 → HTTP → TCP → IP → 网卡 → 网络 → 网卡 → IP → TCP → HTTP → 业务数据
|
② 通信协议流:协议怎么工作
1 2
| TCP:socket → bind → listen → accept → 连接建立 → send/recv → close HTTP:建立 TCP → 发送 HTTP Request → 接收 HTTP Response → Keep-Alive/Close
|
③ 业务运行流:一次业务怎么完成
1 2 3 4
| 用户点击登录 → LoginView → LoginViewModel → AuthService → HttpClient → HTTP Request → TCP → 网络 → HTTP Server → Router → LoginHandler → AuthService → Database → 返回结果 → HTTP Response → HttpClient → LoginViewModel → View
|
数据流是「东西怎么流动」;运行流是「系统行为怎么发生」;业务流是「业务目标怎么完成」。 这几个概念分开之后,网络程序、驱动、游戏、GUI、操作系统都可以用同一套方法分析。
五、启动链也有层级
不是「有启动链的东西都必须由 main 调用」,而是每个启动层的上一级负责调用它的启动链;main 是最顶层启动链的调用者。
1 2 3 4
| main ├── UI.start() 创建窗口 / 创建控件 / 建立 UI 连接 ├── ViewModel.start() 初始化状态 / 建立业务连接 └── Server.start() 初始化 Socket / 初始化 HTTP / 注册 Router / 开始监听
|
如果 ViewModel 是 UI 的组成部分:
1
| main → Client.start() → UI.start() → View + ViewModel.start() → Server.start()
|
main 的核心职责
1
| main = 组装顶层子系统 + 启动顶层子系统 + 进入顶层运行循环
|
1 2 3 4 5 6 7 8 9 10 11 12
| int main(int argc, char* argv[]) { QApplication app(argc, argv);
UI ui; Server server;
ui.start(); server.start();
return app.exec(); }
|
main 不负责创建 Button/Socket/Router/Service/Repository、注册 HTTP Handler——这些分别属于更低一级的启动/组装边界。
1
| 顶层启动 → 子系统启动 → 模块启动 → 组件初始化(上一级调用下一级的启动链)
|
main 是系统顶层组装与启动的入口;子系统负责组装和启动自己的下属;组件负责自己的初始化和运行。 这样代码组装层级、启动链层级、生命周期层级就能对齐起来,而不是全部堆在 main()。
六、有共享状态的 Service:Session 统一拥有共享数据
Service 分成两种情况:无状态 Service(不需要组装,直接做成接口/函数集合)和有共享状态的 Service(必须存在一个统一的状态拥有者)。
方案一:Session 统一拥有共享数据
1 2 3 4 5 6 7 8 9 10
| class Session { public: void setToken(const QString& token) { token_ = token; } const QString& token() const { return token_; } bool loggedIn() const { return !token_.isEmpty(); }
private: QString token_; };
|
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
| class AuthService { public: explicit AuthService(Session& session) : session_(session) {}
bool login(const QString& username, const QString& password) { QString token = requestLogin(username, password); if (token.isEmpty()) return false;
session_.setToken(token); return true; }
private: Session& session_; };
class UserService { public: explicit UserService(Session& session) : session_(session) {}
void loadUser() { QString token = session_.token(); }
private: Session& session_; };
|
三个 Service 都没有自己保存 token:AuthService / UserService / OrderService → Session,共享数据只有一个来源。
谁创建 Session?——组装边界
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class ClientServices { public: ClientServices() : auth_(session_), user_(session_), order_(session_) {}
AuthService& auth() { return auth_; } UserService& user() { return user_; } OrderService& order(){ return order_; }
private: Session session_; AuthService auth_; UserService user_; OrderService order_; };
|
Session + AuthService + UserService + OrderService → ClientServices,ClientServices 就是这个 Service 子系统的组装边界(组件没有组织 → 增加一个文件统一组织 → 从组件变成框架)。
ViewModel 不需要知道 Session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class LoginViewModel { public: explicit LoginViewModel(ClientServices& services) : services_(services) {}
void login(const QString& username, const QString& password) { bool success = services_.auth().login(username, password); if (success) { } }
private: ClientServices& services_; };
|
1 2 3 4 5 6 7 8 9 10 11
| int main(int argc, char* argv[]) { QApplication app(argc, argv);
ClientServices services; LoginViewModel loginVM(services); UserViewModel userVM(services); OrderViewModel orderVM(services); return app.exec(); }
|
main 没有创建 Session/AuthService/UserService/OrderService/HttpClient/Repository——这些都属于 ClientServices 内部的组装。
无状态 Service 的例子
1 2 3 4 5 6 7 8 9 10
| class PasswordValidator { public: static bool valid(const QString& password) { return password.length() >= 8; } };
PasswordValidator::valid(password);
|
最终规则
1 2 3 4 5 6 7 8 9
| Service ├── 无状态 → 函数 / 静态接口 └── 有状态 ├── 状态 → Session / Context / State ├── Service └── 统一组装器
共享状态不要散落到各个 Service 中(错误:每个 Service 各存一个 token) 而应该(正确):Session ← Auth / User / Order
|
无状态能力 → 接口;有状态能力 → 状态对象;多个有状态组件共享 → 统一组织;组织后的整体 → 作为上一级接口提供给 ViewModel。 这就是「什么时候需要组件、什么时候需要接口、什么时候需要组装成框架」的具体判断标准。
七、C 风格函数接口:共享状态用 Context 显式化
如果共享状态存在,组装成对象/子系统通常更自然;但坚持 C 风格的函数接口也完全可以处理。
把共享状态从「对象成员」改成「显式的 Context/State 数据」,函数通过 Context 访问它。
C 写法:Context
1 2 3 4 5
| typedef struct { char token[256]; int user_id; int logged_in; } Session;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int auth_login(Session *session, const char *username, const char *password) { if () { strcpy(session->token, "token123"); session->user_id = 1001; session->logged_in = 1; return 1; } return 0; }
int user_load(Session *session) { if (!session->logged_in) return 0; return 1; }
|
函数本身没有状态,但 Context 是共享状态。
整个程序 Context
1 2 3 4 5 6
| typedef struct { Session session; Config config; Cache cache; Database database; } AppContext;
|
1 2 3 4 5 6 7 8 9 10 11 12
| int main(void) { AppContext app = {0};
app_init(&app);
auth_login(&app, "alice", "123456"); user_load(&app); order_load(&app);
app_shutdown(&app); }
|
C 和 C++ 是同一个东西
1 2 3 4
| C++:Service → 成员/引用 → State C: Function → 参数 → State
C 没有类,并不意味着没有组件组装。只是把「对象之间的关系」显式变成了结构体 + 指针 + 函数。
|
全局变量的代价
1
| static Session g_session;
|
参数没了,但依赖关系隐藏了——看到 load_user(); 并不知道它依赖 g_session;而 load_user(&session); 一眼就知道。
1 2 3
| 全局变量:最少参数,但依赖隐藏 Context* :多一个参数,但依赖显式 类成员 :参数少 + 状态封装
|
对象封装会隐藏共享状态的传递;纯函数接口会把依赖显式暴露出来。C 的函数式 API 并没有消灭「组件—状态—组装」思想,只是没有 C++ 的对象语法而已:数据结构 = 状态,函数 = 能力,Context = 组件之间的共享连接,初始化 = 组装,函数调用 = 运行,shutdown = 生命周期结束。
八、共享指针:不复制,传同一个地址/句柄
1 2 3 4 5 6 7 8 9 10 11 12 13
| typedef struct { int value; } SharedData;
void service_a(SharedData *data) { data->value += 10; } void service_b(SharedData *data) { printf("%d\n", data->value); }
int main(void) { SharedData data = { .value = 100 }; service_a(&data); service_b(&data); }
|
多个共享指针时用 Context:
1 2 3 4 5 6 7 8 9 10
| typedef struct { Session *session; SharedBuffer *buffer; Database *database; Config *config; } AppContext;
void login(AppContext *ctx); void load_user(AppContext *ctx); void send_data(AppContext *ctx);
|
层层调用时指针沿调用链传递:handler → service → repository → database(没有创建新的共享数据,只是把同一个 AppContext* 传下去)。
实用判断
1 2 3 4
| 共享一个数据 → 直接传指针 共享几个相关数据 → 传一个结构体指针 大量组件共享运行环境 → Context Context + 函数集合 + 初始化/销毁 → 逐渐形成一个 C 风格的子系统
|
C 语言虽然没有 C++ 的对象,但一样可以做「组件 + 共享状态 + 组装 + 生命周期」:只是把隐含在对象里的东西显式写成了 struct + pointer + function。
对象 vs 函数接口的核心差异
1 2 3 4 5 6 7
| C++:class Service { int value_; void a() { value_++; } void b() { value_++; } }; 编译器通过隐藏的 this 让成员函数访问状态,不需要写参数
C: void service_a(Context* ctx); void service_b(Context* ctx); 共享状态必须作为显式参数传递
对象封装隐藏共享状态的传递;纯函数接口把依赖显式暴露出来。
|
有共享数据的 Service 更适合组装成对象,因为对象本质上就是在做一件事:把经常共同传递的数据和操作这些数据的函数绑定在一起,从而把重复参数变成对象状态。
九、服务器的完整形态
1 2 3 4 5 6 7 8 9
| main → 实例化/启动 → Server Server ├── Protocol HTTP / WebSocket / RPC ├── Network Socket / Connection / EventLoop / Buffer ├── Parser ├── Dispatcher / Router └── Handler / Calling Layer ↓ Business Library(Service + Domain + Algorithm + Business State)
|
服务器负责「解析、分发、调用」,业务负责「完成业务」。
1 2
| POST /login → HTTP Request → Server → HTTP Parser → Router → LoginHandler → LoginService → Domain/Algorithm → 返回业务结果 → Handler → HTTP Response
|
业务做成静态库
1 2 3 4 5 6
| business/ ├── include/ LoginService.h / UserService.h / OrderService.h ├── src/ LoginService.cpp / UserService.cpp / OrderService.cpp ├── domain/ ├── algorithm/ └── CMakeLists.txt → libbusiness.a
|
1 2 3 4 5 6 7
| #include <business/LoginService.h>
void LoginHandler::handle(const Request& request) { auto result = LoginService::login(request.username(), request.password()); }
|
服务器本身不需要知道登录业务内部怎么实现。
服务器不是「所有业务的集合」,而是一个业务执行平台/通信运行系统;业务静态库则是被这个平台调用的业务能力。 服务器完整版:通信基础设施 + 协议处理 + 请求解析 + 请求分发 + 业务调用 + 响应生成 + 生命周期管理。
十、Network ≠ OSI 会话层;生命周期 ≠ 代码结构
Network 不等于「会话层」
Server → Network(Socket/Connection/EventLoop/Buffer) 是程序内部的网络运行子系统,不是 OSI 七层模型中的某一层,它可能跨越多个 OSI/TCP-IP 层次:
1
| 应用程序 → HTTP(应用层协议)→ TCP/UDP(传输层)→ IP(网络层)→ Socket API(编程接口)
|
会话层(Session Layer)是 OSI 第 5 层,负责会话建立、维护、终止等概念。Network ≠ Session Layer。但产生这个联想是有道理的:Connection 的建立/运行/关闭生命周期确实具有「会话」的特征。
生命周期是不是「封装组件形成框架的代码」?
需要区分:
生命周期不是代码组织结构本身,而是被代码实现出来的一条时间维度。
1 2
| 结构维度:组件 → 组装 → Server(Socket + Router + EventLoop → Server) 时间维度:创建 → 初始化 → 启动 → 运行 → 停止 → 销毁
|
1 2 3 4 5 6 7 8
| void Server::start() { socket_.open(); router_.initialize(); eventLoop_.start();
running_ = true; }
|
这里同时发生两件事:结构(Server 由 Socket/Router/EventLoop 组成)和生命周期(Server.start() 组织各组件打开/初始化/启动)。
框架不仅封装组件,还负责组织这些组件的生命周期、调用关系和运行流程。
1 2 3 4 5 6 7
| 组件(Socket/Connection/Buffer/EventLoop) ↓ 结构组装 Network 子系统(初始化/启动/运行/停止 ← 生命周期) ↓ Server(HTTP Protocol + Parser + Router + Handler) ↓ 业务 Service
|
「架构结构」描述空间关系;「生命周期」描述时间关系;「调用链」描述控制关系;「数据流」描述数据关系;「业务流」描述业务行为关系。 一个真正完整的工程分析,往往就是把这几张图叠起来,而不是只画一张「分层架构图」。
十一、最终原则
以后分析一个程序,可以分别问:
1 2 3 4 5 6 7
| - 结构:有哪些组件、子系统,在哪里组装? - 调用链:谁调用谁? - 启动链:从 main 到各子系统如何启动? - 生命周期:创建 → 初始化 → 运行 → 停止 → 销毁。 - 数据流:数据经过哪些对象、协议、层? - 业务流:一次业务从入口到完成经过什么路径? - 状态变化:系统/对象的状态如何变化?
|
(共七个维度:结构、调用链、启动链、生命周期、数据流、业务流、状态变化。)
组件负责能力,组装负责关系,框架负责生命周期和运行组织,接口负责对外能力;上一级只组装下一级的整体。
这套思路拿去看 GUI、服务器、游戏引擎、嵌入式程序、Linux 子系统,基本都能套上。
十二、这一条线的位置
1 2 3 4 5 6 7 8 9 10 11 12
| 设计(主题): 01 设计起点(先设计什么) 02 系统设计(谁来设计) 03 设计表示(怎么表示) 04 文档组织(怎么落成文档) 05 通信程序的设计顺序(设计线实例) 06 七个分析维度(本线:怎么分析一个程序——设计的逆向)
七条流 / 行为流: 七个维度的「启动链、数据流、业务流、状态变化」与行为十条流互补: 行为流讲行为怎么发生(控制/错误/反馈…),七个维度讲程序怎么被分析 (启动链 → 启动流;业务流 → 业务流;状态变化 → 状态流)
|
本线与 05 的关系:05 讲通信程序「按什么顺序设计」(协议 → 组件 → 接口 → 实现 → 框架化 → 组装);本线讲分析任何程序「从哪些维度看」(结构/调用链/启动链/生命周期/数据流/业务流/状态变化)。设计是正向展开,分析是逆向还原,两者是同一套结构的两个方向。
收束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 七个分析维度: 结构(在哪里组装)→ 调用链(谁调用谁)→ 启动链(怎么起来) → 生命周期(怎么存在)→ 数据流(数据怎么走) → 业务流(业务怎么完成)→ 状态变化(状态怎么变)
三种流不能混: 数据流(东西怎么流动)/ 协议流(协议怎么工作)/ 业务流(业务怎么完成)
共享状态的判断标准: 无状态 → 接口(函数/静态接口) 有状态 → 状态对象(Session/Context)+ 统一组装器(组装边界) 共享多个 → Context 显式化(C)或类封装(C++)
C 与 C++ 是同一个东西: 数据结构 = 状态,函数 = 能力,Context = 共享连接, 初始化 = 组装,函数调用 = 运行,shutdown = 生命周期结束
最终原则: 组件负责能力,组装负责关系,框架负责生命周期和运行组织, 接口负责对外能力;上一级只组装下一级的整体
|
七个分析维度 = 分析任何程序的完整方法:结构/调用链/启动链/生命周期/数据流/业务流/状态变化七张图叠起来,才能看到程序的全貌。 它适用于 GUI、服务器、游戏引擎、嵌入式、Linux 子系统——是「设计的逆向」。