第 2 章:搭起骨架——任务模型与程序边界
从 Task 数据模型开始,把命令解析、业务动作和文件存储拆成可以独立理解与测试的边界。
项目路线Go 本地待办清单 CLI2 / 5
从一条任务开始建模
任务需要稳定编号、标题、完成状态和创建时间。对应的 Go 结构体很直接:
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
}
字段不是越多越好。只有当前需求会读取或修改的数据才进入模型。创建时间暂时不展示,但它是后续排序和迁移的重要事实,因此在创建时就保存。
理解程序的三个边界
程序被拆成三层,但它们不是传统意义上的大型分层架构:
main读取环境变量和命令行参数,组装程序。App理解add、list、done、delete的业务含义。Store只负责把[]Task从文件读出或写回。
这样拆分后,测试可以直接调用 App.Run,传入一组参数和一个内存缓冲区,不必真的启动新的终端进程。
让时间也成为依赖
创建任务时需要当前时间。如果在业务代码中到处调用 time.Now(),测试结果会随运行时刻变化。把时间函数放到 App:
type App struct {
Store Store
Now func() time.Time
}
正式运行传入 time.Now,测试传入固定时间。这是依赖注入最朴素的形式:不需要框架,只需要把变化来源明确交给调用者。
设计统一入口
Run 先加载任务,再根据第一个参数分派命令:
func (app App) Run(arguments []string, output io.Writer) error {
if len(arguments) == 0 {
return errors.New("用法: todo add|list|done|delete")
}
tasks, err := app.Store.Load()
if err != nil {
return err
}
switch arguments[0] {
case "add":
return app.add(tasks, arguments[1:], output)
case "list":
return app.list(tasks, output)
case "done":
return app.setDone(tasks, arguments[1:], output)
case "delete":
return app.delete(tasks, arguments[1:], output)
default:
return fmt.Errorf("未知命令 %q", arguments[0])
}
}
io.Writer 让输出目标可替换:正式程序传 os.Stdout,测试传 bytes.Buffer。
本章验收
创建 go.mod、task.go、app.go、store.go 和 main.go,确保 go test ./... 至少可以编译。此时命令还不必全部工作,但你应该能画出输入从 main 进入 App、再访问 Store 的方向。
JARVIS · 当前文章
有哪里没看懂?可以只问这篇。
Jarvis 会限定在《第 2 章:搭起骨架——任务模型与程序边界》及其公开关联内容中检索,并把引用定位回原文章节。