从指令到业务系统——软件组织的第一条演化线

从一条指令开始,经过结构化、函数、文件、分层,最终长成一个业务系统——由模块组成,每个模块 = 数据结构 + 算法 + 接口。这是所有软件的第一条演化路径。


一、从一条指令开始

最开始,程序就是几条连续的指令:

1
2
3
MOV  EAX, 5       ; 把 5 放进寄存器
ADD EAX, 3 ; EAX = EAX + 3
MOV [addr], EAX ; 结果存进内存

CPU 按地址顺序取指令,一条执行完再执行下一条。没有分支、没有循环、没有函数。


二、第一次拐弯:判断与跳转

要让程序根据情况选择不同处理,需要比较 + 条件跳转

1
2
3
4
CMP  EAX, 10
JGE label_done
ADD EAX, 1
label_done:

程序第一次有了「逻辑」——根据输入/状态,走不同的路。这就是 if-else 的硬件本质


三、重复与复用:循环和函数雏形

循环 = 往回跳

1
2
3
4
5
6
7
MOV  ECX, 0
loop_start:
CMP ECX, 10
JGE loop_end
ADD ECX, 1
JMP loop_start ; 往回跳
loop_end:

函数 = 跳过去再跳回来

1
2
3
4
CALL add_three    ; 跳到函数,压栈返回地址
add_three:
ADD EAX 3
RET ; 弹栈,跳回

循环让程序能处理任意数量的数据;函数让一段逻辑能被多处调用


四、C 语言:把硬件包装成人类语言

C 语言把汇编的跳转和栈操作,包装成三大结构:

1
2
3
4
5
6
int main() {
int a = 5, b = 3, c = a + b; // 顺序
if (c > 10) { printf("big\n"); } // 判断
for (int i = 0; i < 10; i++) { c += i; } // 循环
return 0;
}

但 C 语言刚诞生时,所有代码都写在 main() 里。


五、main 膨胀:职责混乱的起点

功能增加后,main 开始膨胀——接收参数、读文件、多个业务逻辑、输出格式化全挤在一起。改任何一个都要在 main 里找半天。

职责混乱是拆分的第一个驱动力,但不是唯一一个。 拆分的驱动力有四种,后面的每一步拆分都是其中一种在起作用:

1
2
3
4
职责混乱 → 拆分(一件事做了太多事)
重复代码 → 封装(很多地方做同一件事)
边界 → 分层(两部分因不同原因变化 / 有不同生命周期)
依赖混乱 → 接口(依赖需要隔离)

接下来的章节,每一步拆分都会标注它由哪种驱动力逼出来。


六、第一个拆分:函数

把 main 里的职责拆成独立函数:

1
2
3
4
5
6
7
int main(int argc, char** argv) {
Args args = parse_args(argc, argv);
Data data = read_file(args.path);
Result r = do_command(args.cmd, &data);
print_result(&r);
return 0;
}

main 变成了流程描述——只说「做什么」,具体实现藏进函数。


七、第二个拆分:文件

驱动力:扩展。 一个文件装不下所有函数了,职责从一处分到多处。

按职责把函数分到不同文件,用 .h 声明接口、.c 放实现:

1
2
3
4
main.c      → main() + parse_args()
file_io.c → read_file()
scan.c → do_scan()
list.c → do_list()

.h 文件就是接口的第一次物理出现——只写「能做什么」,不写「怎么做」。


八、第一次分层:入口与业务分离

把「接收参数」和「具体处理」拆到不同层:

1
2
3
4
5
6
7
8
9
// main.c(入口层)—— 只管「接收、调用、输出」
int main(int argc, char** argv) {
Args args = parse_args(argc, argv);
Result r = do_scan(&data); // 调用业务层
print_result(&r);
}

// scan.c(业务层)—— 只管扫描逻辑
Result do_scan(const Data* data) { ... }

main.c 只管「怎么接收、调用谁、怎么输出」;业务层只管「具体怎么处理」。

驱动力:边界。 入口会因交互方式变化,业务会因业务需求变化——两者变化原因不同,所以拆开。所有拆分(拆函数、拆文件、拆系统)都沿边界切,边界的完整类型有独立的线展开。


九、入口的骨架:接受 → 解析 → 分发 → 调用

驱动力:扩展。 main.c 里「解析参数」和「知道调用哪个函数」混在一起,加第二个命令就爆炸。拆成四个阶段:

1
2
3
Request req = parse(argc, argv);           // 解析
Handler h = dispatch(req.cmd, handlers); // 分发
Result r = h(&data); // 调用
1
接受 → 解析 → 分发 → 调用 → 业务

十、业务按功能分模块

驱动力:职责。 业务层里的 scan.c 既有「读进程列表」又有「扫描内存」,混在一起。按功能切

1
2
3
process_list.c / .h   → 进程列表相关
memory_scan.c / .h → 内存扫描相关
scan.c / .h → 调度(只调用上面两个模块)

十一、依赖方向:谁可以调用谁

驱动力:依赖。 分层后,依赖方向必须单向

1
2
3
4
入口层 → 业务层 → 基础设施层

✅ 上层调用下层
❌ 下层调用上层(禁止)

进一步,当业务层只依赖接口、不依赖具体实现时,就出现了依赖倒置

1
业务层 → 接口 ← 基础设施层

依赖关系的完整演化有独立的线展开。


十二、模块的三要素:数据 + 算法 + 接口

模块拆出来后,内部至少包含三个不同的东西:

1
2
3
4
5
6
7
8
9
10
11
typedef struct { uint8_t* bytes; size_t size; } MemoryBlock;  // 数据结构

ScanResult search_pattern(const MemoryBlock* mem, const uint8_t* pat, size_t len); // 算法

ScanResult scan_memory(const char* path, const uint8_t* pattern, size_t len) {
int fd = open(path, O_RDONLY); // 库提供的接口
MemoryBlock block = { (uint8_t*)mem, st.st_size };
ScanResult r = search_pattern(&block, pattern, len); // 算法
close(fd);
return r;
}

功能函数 = 数据结构 + 算法 + 库提供的接口。 这是模块的基本形态。

算法本身还要分类型——不同算法表达重点不同:步骤类用流程图,递归类用递归树,数学类用公式推导,状态转移类用状态转移表。形式的选取独立成线。


十三、面向对象:封装与类

驱动力:重复。 数据和操作它的函数散在各处,同样的状态处理重复出现。C++ 用类把数据和操作它的函数包在一起:

1
2
3
4
5
6
7
8
9
class MemoryBlock {
public:
MemoryBlock(const std::string& path);
~MemoryBlock();
ScanResult search(const uint8_t* pat, size_t len);
private:
uint8_t* bytes_;
size_t size_;
};

类带来的不止「封装」一件事,类的作用有五个:

1
2
3
4
5
1. 封装      谁能碰内部数据(private 挡住外部)
2. 生命周期 构造创建、析构释放(RAII)
3. 状态管理 成员变量随对象存在,每个对象有自己的一份
4. 访问控制 public / private 决定谁能调什么
5. 模块边界 类 = 一个独立的组件边界,区分职责

前两个(封装、生命周期)最常被提到,但后三个同样重要——状态管理让对象有自己的数据,访问控制把接口和实现分开,模块边界让类成为组件的基本单元。

命名空间的作用和类不同:类区分组件边界,命名空间区分概念归属

1
2
3
network::Connection
database::Connection
gui::Connection

三个都叫 Connectionnetwork:: 表示「网络领域的 Connection」。命名空间不只是封装库——它给名称建立一层概念边界,让 app::cli::app::domain::app::infrastructure:: 这些名称直接反映系统结构。

当「同一段逻辑,只是类型不同」的函数越写越多时,C++ 的模板把类型也变成参数——一个函数覆盖所有类型。这是「重复代码就封装」在类型维度上的延伸,独立成线。


十四、用类实现分层

有了类,分层从文件之间的分层变成了类之间的分层

1
2
3
4
Application(最外层组装)
→ ViewModel(接收 UI 事件,调用 Service)
→ Service(业务逻辑)
→ Infrastructure(文件/网络/内存)

Application 只调 service_.listProcesses(),不碰 processList_——层级边界由类的访问控制保证。


十五、从模块到业务系统

驱动力:边界。 模块拆出来是零散的——每个模块只做自己的事,但没人把它们组织成一个整体。业务层需要作为一个系统被入口调用,于是模块被组织成业务系统。

这一步做四件事:

1
2
3
4
1. 建立模块层级      按业务领域和依赖方向把模块组织成层级
2. 建立模块关系图 模块之间怎么依赖、怎么调用
3. 功能—模块映射 每个功能都能定位到某个模块
4. 模块—系统映射 每个模块归属到正确的业务系统
1
2
3
4
5
6
7
8
9
10
业务系统
├── 模块层级:
│ ├── service/ 业务任务组织(调用下面各模块)
│ ├── process_list/ 进程列表模块(数据+算法+接口)
│ ├── memory_scan/ 内存扫描模块(数据+算法+接口)
│ └── file_io/ 文件读写模块(数据+算法+接口)

├── 模块关系:service → process_list / memory_scan / file_io
├── 功能—模块映射:每个功能都能找到归属模块
└── 模块—系统映射:每个模块属于这个业务系统

关键:业务系统的组织单位是模块,模块 = 数据结构 + 算法 + 接口。 模块组成业务系统,业务系统作为一个整体被入口调用。

业务系统可以做成库:

1
2
3
4
5
6
business/
├── include/ list_processes.h / scan_memory.h
├── src/ list_processes.cpp / scan_memory.cpp
├── domain/ ProcessInfo / MemoryBlock
├── algorithm/ search_pattern
└── CMakeLists.txt → libbusiness.a

入口系统(CLI/GUI/Server)调用它,不需要知道内部有哪些模块:

1
2
3
#include <business/scan_memory.h>

Result r = scan_memory(path, pattern, len); // 业务系统作为一个整体

到这里,第一条演化线完成:从一条指令,经过函数、文件、分层、模块,最终长成一个业务系统。 业务系统由模块组成,模块由数据结构、算法、接口组成。


收束:从一条指令到业务系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
一条指令        顺序执行
判断跳转 第一次「选择」
循环 / 函数 重复 + 复用
C 语言 三大结构的编程语言形态
main 膨胀 职责混乱
拆分函数 第一个拆分动作
拆分文件 编译单元 + .h 接口
分层 入口与业务分离
入口细分 接受→解析→分发→调用
业务分模块 按功能切分
依赖方向 分层后必须单向(上→下)
模块三要素 数据 + 算法 + 接口
OOP / 类 封装 + 生命周期 + 状态管理 + 访问控制 + 模块边界
命名空间 名称隔离 + 概念归属(network:: vs database::)
类分层 ViewModel / Service / Infrastructure
模板 / 泛化 同功能不同类型 → 类型维度上的封装(独立线)
边界 所有拆分沿边界切:职责/变化/生命周期/依赖/物理(独立线)
模块→业务系统 模块层级 + 关系 + 功能/模块映射 → 业务系统(由模块组成)

每一步都是上一步「职责混乱 / 重复 / 边界 / 依赖」四种驱动力之一逼出来的。 理解了演化过程,就理解了业务系统为什么长成现在这个样子。

下一篇看第二条演化线:同样的路径,但终点是网络系统由组件组成。