02-软件开发的最小闭环
软件开发的最小闭环:自顶向下设计、自底向上实现、步步可验证
软件开发的核心不是「设计 → 全部实现 → 最后一次运行」,而是一圈圈可验证的最小闭环:自顶向下设计(整体结构先定),自底向上实现(从最底层最可验证的单元开始),每一步都是「动作 → 产生结果 → 验证结果 → 解锁下一步」。编程学习也是一样——一个个可以不断编码、执行、验证的最小闭环。这一篇讲最小闭环在软件开发里怎么落地。
一、先看反面:为什么「全部写完再运行」会失败123456❌ 错误流程: 设计全部 → 写完全部组件 → 写完全部子系统 → 最后一次运行 → 崩了,不知道哪里错
问题:每一步都没有验证,错误堆积到最后一次性爆发。 等系统复杂以后,「最后一次运行」几乎必然痛苦。
1234567✅ 正确流程: 设计整体(自顶向下) → 拆分 → 为每个层级建立局部闭环 → 一边实现一边验证 → 逐层集成 → 最终系统验证
设计可以大,实现必须小步。 每一步都落在某个闭环里,才谈得上「可验证」。
二、自顶向下设计:先定结构设计方向是自顶向下——整体先定:
1234567891011需求 ↓总体方 ...
01-最小闭环
最小闭环:输入 + 处理 + 输出 + 验证
最小闭环 = 最小输入 + 处理 + 输出 + 验证手段。它是一切「演化、验证、学习、工程」的基本单位——所有更大的系统,都是从这个能验证的小环,一圈圈向外扩出来的。这一篇讲概念本身:闭环是什么、怎么判断、怎么扩大。
一、从小例子讲起:一个函数就是一个闭环1输入 → 函数 → 输出 → 断言
一个函数:喂入参,跑一遍,断言结果——这已经是一个闭环。它不需要整个程序跑起来才能被验证。
123// 最小闭环:输入 2 和 3 → 处理(相加)→ 输出 5 → 验证(断言)int add(int a, int b) { return a + b; }assert(add(2, 3) == 5); // 闭环完成
函数的价值:它把「做一件事」和「验证这件事」绑在一起。 跑一遍、断言通过,这件事就闭环了。
二、抽出模型:闭环 = 输入 + 处理 + 输出 + 验证1闭环 = 该层级具备「最小输入、处理、输出、验证手段」
判断一个东西是不是闭环,只看它能不能独立地「做完 → 知道自己做对了没有」: ...
06-交互形态下其他流的变化
交互形态下其他流的变化——控制流/错误流/反馈流/状态流
从输入到交互主线的五篇分别讲五种交互形态的用户流程(用户操作什么、得到什么)。但同一批交互形态下,还有四条其他流也在演化:控制流(程序往哪走)、错误流(出错怎么办)、反馈流(发现问题回退到哪)、状态流(跨操作记住什么)。它们散落在五篇各节里,本篇把它们合并成一条流:按流纵向追踪,五种形态横向对比。
一、为什么需要分离五篇主线的焦点是用户流程:用户可感知的操作序列。
12345无交互CLI:输入 → 等待 → 结果 → 结束有交互CLI:启动 → [输入 → 执行 → 输出] × N → exitTUI: 启动 → 绘制 → [按键 → 焦点 → 重绘] × N → 退出GUI: 启动 → 窗口 → [操作控件 → 响应 → 更新] × N → 关闭多系统GUI:启动 → 窗口 → [操作 → 可能跨进程 → 更新] × N → 关闭+清理
四条其他流在同样的形态变迁中各自演化,但不是「用户做了什么」,而是「程序内部/程序与用户之间发生了什么」:
123456控制流 — ...
05-多系统GUI
多系统GUI——用户流程跨越进程边界
单个GUI窗口的用户流程在一个进程内闭环:用户操作→控件响应→窗口更新。但真实的GUI程序往往需要和其他进程协作——调用编译器、访问数据库、连接网络服务、加载外部资源。当GUI需要和多个外部系统交互时,用户流程第一次跨越了进程边界。
一、从单进程到多进程单进程GUI:
1234用户操作 GUI → GUI 进程处理 → GUI 进程更新窗口 → 用户看到结果
只有一个进程,所有操作都在同一个进程内完成。
多进程GUI:
1234567用户操作 GUI → GUI 进程收到操作 → GUI 进程发送请求给编译器进程 → 编译器进程编译代码 → 编译器进程返回结果 → GUI 进程更新窗口 → 用户看到结果
GUI进程和编译器进程是独立的进程——它们有自己的内存空间、自己的生命周期、自己的状态。
多系统GUI的核心变化:用户操作的后果跨越了进程边界。
二、进程间通信:GUI与外部系统的桥梁GUI进程和外部系统之间的通信方式:
1234567891011管道(Pipe): GUI → 写入管道 → 编译器进程读取 → 编译器 ...
04-GUI
GUI——用户流程从屏幕网格到自由窗口
TUI在终端的字符网格上划分区域,焦点在区域之间转移。GUI打破了网格的限制——窗口可以任意大小、任意位置、任意层叠;控件可以是按钮、滑块、下拉菜单;用户用鼠标点击而不是键盘导航。用户流程从「在网格中移动焦点」变成了「在窗口中操作控件」。
一、从网格到自由布局TUI的屏幕是字符网格——每个位置只能放一个字符,区域只能是矩形。
GUI的屏幕是像素——任意位置可以放任意形状的控件,窗口可以层叠、缩放、拖拽。
123456789101112131415161718192021TUI:┌────────────────────────┐│ Region A │ Region B ││ │ │├──────────┴─────────────┤│ Region C │└────────────────────────┘GUI:┌──────────────┐│ Window A │ ← 可以拖拽、缩放│ ┌────────┐ ││ │ Button │ │ ...
03-TUI
TUI——用户流程从文本循环到屏幕交互
有交互CLI的用户流程是一个文本循环:输入命令→输出文字→再输入。但当程序需要让用户「浏览列表」「选择项目」「查看详情」时,纯文本的命令循环就力不从心了。TUI在终端屏幕上划分区域,用户在区域之间移动焦点、触发操作——用户流程从「输入命令」变成了「在屏幕上导航」。
一、从命令到屏幕有交互CLI:
123456789> list1. process A (PID 100)2. process B (PID 200)3. process C (PID 300)> select 2selected: process B> detailsPID: 200, Memory: 45MB, Threads: 12> exit
用户需要记住自己看到了什么、选择了什么、下一步要做什么什么。信息是线性的,上下文靠记忆维持。
TUI:
1234567891011┌──────────────────────────────┐│ Processes [v1.0]│├──────────────┬────────── ...
02-有交互CLI
有交互CLI——用户流程第一次变成循环
无交互CLI的用户流程是一条直线:输入→输出→结束。有交互CLI打破了这个模式——程序启动后不退出,用户可以反复输入命令,每次输入都得到输出,直到用户主动说「退出」。用户流程从直线变成了循环,这一个变化带来了全新的设计问题。
一、从直线到循环无交互CLI:
123$ grep "hello" file.txthello world$ ← 程序已退出
有交互CLI:
123456789$ tool> scan file.binfound at offset 42> statusrunning, 3 threads> scan another.binnot found> exit$ ← 用户主动退出
区别:
1234567无交互CLI: 启动 → 执行 → 输出 → 退出 (直线)有交互CLI: 启动 → [输入 → 执行 → 输出 → 再输入 → 执行 ...
01-无交互CLI
无交互CLI——用户流程的最简形态
从最简单的程序开始。用户输入一行命令,程序运行,输出结果,结束。用户流程在这里是一条直线——没有状态,没有回退,没有选择。但正是这条直线,定义了所有后续交互形态的起点。
一、一条直线123$ grep "hello" file.txthello world$
用户做了三件事:
1231. 输入命令2. 等待3. 看到输出
程序做了三件事:
1231. 接收参数2. 执行3. 输出结果
两者之间的关系:
1用户输入 → 程序执行 → 用户看到结果
没有中间状态。 程序运行期间用户不能干预,用户输入之后程序不能追问。整个交互过程就是一条直线。
二、直线的结构123456789101112sequenceDiagram participant U as 用户 participant T as 终端 participant P as 程序 U->>T: 输入命令行 T->>P: argc, argv Note over P: 执行中...<br/> ...
03-多系统组成的网络通信软件
多系统组成的网络通信软件——三个系统如何组合
前两篇分别演化出了业务系统(由模块组成)和网络系统(由组件组成)。现在要把它们组合成一个完整的网络通信软件。注意:网络通信软件分客户端和服务器,组成不同——客户端是「入口系统 + 网络系统」两个系统,服务器是「入口系统 + 业务系统 + 网络系统」三个系统。这一篇以服务器为例展开,客户端的组成在第九节说明。
一、两个系统的矛盾单体程序跑得好好的,加了网络后,网络逻辑和业务逻辑混在一起:
1234inspect_program(单体) ├── 读文件、扫描内存(业务逻辑) ├── 收发字节、管理连接(网络逻辑) └── 解析参数、输出结果(入口逻辑)
网络代码改了影响业务,业务改了影响网络。两个系统需要独立演化、独立测试。
二、拆成三个系统先分清两种角色,组成不同:
1234567891011客户端 = 入口系统 + 网络系统(两个系统) 客户端发起请求,业务逻辑在服务器端 入口系统:用户交互(输入 → 请求、响应 → 展示) 网络系统:收发字节 没有独立的业务系统——客户端的「业务」很薄,并入入口系统服务器 = 入口系 ...
02-从函数到网络系统
从函数到网络系统——软件组织的第二条演化线
同样的演化路径——函数、文件、分层、系统——但终点不同。这次的终点是网络系统:由有状态组件 EventLoop/Connection/Session、无状态工具 Buffer、协议数据结构 Protocol 组成。组件和模块的演化逻辑相同,但领域不同。
一、一条 socket 调用网络的起点和业务系统一样简单——一条调用:
12345int sock = socket(AF_INET, SOCK_STREAM, 0);connect(sock, (struct sockaddr*)&addr, sizeof(addr));send(sock, "hello", 5, 0);recv(sock, buf, sizeof(buf), 0);close(sock);
所有东西在一个函数里——创建、连接、发送、接收、关闭。和「所有东西写在 main 里」是同一个阶段。
二、循环处理驱动力:重复。 要处理多次请求,需要循环:
123456while (1) { int client ...
