设计起点——外部交互边界决定第一设计对象

一句话

不同程序的「外部交互边界」不同,因此第一设计对象也不同。先设计程序「怎么被外部使用」,再设计内部「怎么实现」。

拿到一个新项目,第一步容易迷失在「先写哪个类、哪个文件、哪个函数」里。设计起点告诉你:先回答「这个程序怎么被外部使用」,再逐层向内设计。


一、外部交互边界决定第一设计对象

程序类型 最先设计的核心东西 它实际上定义了什么
CLI 入口参数 / 命令 用户如何调用程序
通信程序 通信协议 / 消息 两端如何约定通信
GUI UI / 用户操作流程 用户如何操作程序
TUI UI / 用户操作流程 用户如何操作程序
API / 接口 其他程序如何调用它
服务器 协议 + 请求模型 外部如何请求服务
驱动 驱动接口 / I/O 模型 OS 或上层如何使用驱动

每一行都指向同一个动作:程序与外部世界「打交道」的那个边界

  • 边界在命令行 → 第一设计对象是命令与参数(CLI)
  • 边界在两端通信 → 第一设计对象是协议与消息(通信程序)
  • 边界在用户操作 → 第一设计对象是 UI(GUI / TUI)
  • 边界在程序间调用 → 第一设计对象是 API(库 / 服务器 / 驱动)

所以不能笼统说「所有软件都是先设计 UI / 接口」,而要看外部交互边界具体落在哪里


二、四类程序的展开

1. CLI 先设计入口参数

做一个 image-tool

1
image-tool input.png --resize 800x600 --output out.png

首先确定的是:命令 → 参数 → 参数含义 → 输出:

1
2
3
4
image-tool
├── input
├── resize
└── output

然后才往里设计:参数解析 → 业务调用 → 算法 → 文件输出。

CLI 的外部契约就是命令行接口。 外部世界(用户 / Agent / 其他脚本)通过命令与参数控制系统,所以第一设计对象是入口参数,而不是内部类、内部函数。

2. 通信程序先设计协议

客户端和服务器做登录,先确定消息格式:

1
2
3
4
5
Client → Server
LOGIN { username, password }

Server → Client
LOGIN_RESULT { success, token }

然后才设计:协议 → 解析 → 分发 → Handler → Service → Domain。

两个独立运行的系统如何达成共同理解? 两端各自独立运行、互不共享内存,唯一能对齐的就是协议。协议先行,解析、分发、业务处理都建立在协议之上。

3. GUI 先设计 UI

GUI 的入口是用户操作。首先需要知道界面的组成:

1
2
3
4
5
6
窗口
├── 菜单
├── 工具栏
├── 输入框
├── 按钮
└── 内容区域

以及用户操作发生什么:

1
用户点击按钮 → 发生什么 → 数据怎么变化 → 界面怎么更新

然后再设计 View / ViewModel / Service / Domain / Infrastructure。

「可以先只画窗口和 UI 在 View 层,然后数据分离吗?」其实就是这个思想:UI 定义了程序与用户的交互边界,边界确定后再决定数据如何变化、内部如何分层。

4. TUI 本质上也是先设计 UI

有状态 TUI 更接近 GUI,而不是 CLI。例如进程列表界面:

1
2
3
4
5
6
7
8
┌──────────────────────────────┐
│ Processes │
├──────────────┬───────────────┤
│ PID Name │ Details │
│ 100 abc.exe │ Memory... │
├──────────────┴───────────────┤
│ ↑↓ Select Enter Open q Quit│
└──────────────────────────────┘

首先要确定:界面有哪些区域、有哪些状态、用户有哪些操作、焦点怎么移动、页面怎么切换。然后才是 TUI View → 状态 → Controller/ViewModel → Service → Domain。

无状态 CLI → 先设计命令接口。有状态 TUI → 先设计交互界面。

这也是为什么「CLI 很适合 Agent,但调试器必须有调试会话和状态」——调试器是典型的有状态程序,会话与状态是它的核心,不能退化成纯命令式 CLI。


三、统一规律:先设计系统对外的控制面

把上面全部收拢成一句:

先设计「系统对外的控制面」。

不同程序的控制面不同:

1
2
3
4
5
6
7
CLI          → Command / Argument
TUI → Interactive UI
GUI → Interactive UI
通信程序 → Protocol / Message
库 → API
驱动 → Driver Interface
服务器 → Protocol + Request

然后才往里面倒推:

1
2
3
4
5
6
7
8
9
10
11
外部接口

控制流

业务

数据

算法

基础设施

这个倒推结构和一个更早的结论接得上:接口决定「怎么控制系统」,架构决定「系统内部怎么实现这个控制」。


四、为什么 CLI + 静态库特别容易闭环

这也解释了为什么 CLI + 静态库 特别容易形成闭环:

  • CLI 提供控制入口(控制面)
  • 静态库提供稳定能力(实现面)
  • 两者之间的边界非常清楚

边界清楚,双向演化(CLI 反馈需求 → 静态库补充能力)就不容易失控。


五、实用原则

一句原则:

先设计程序「怎么被外部使用」,再设计内部「怎么实现」。

向内拆解链条:

1
入口 → 控制流程 → 业务 → 数据 → 算法 → 基础设施 → 具体实现

用途:

这样拿到一个新项目,第一步就不容易迷失在「先写哪个类、哪个文件、哪个函数」里——先回答「这个程序怎么被外部使用」,再逐层向内设计。


与其他线的关系

  • 与系统角色主题:系统角色决定程序由哪些角色组成;设计起点决定这些角色中先设计哪一个的对外控制面
  • 与从输入到交互线:从输入到交互讲用户流程的形态(运行时);设计起点讲该形态对应的程序先设计什么(设计时)
  • 与服务器线:服务器的控制面是「协议 + 请求模型」,与设计起点的「通信程序先设计协议」一致