早期软件项目的开发策略

前言

最近在进行一些软件产品(KVStore和一个UI框架,所以后面可能会有相关例子)从0到1的开发,时常对某些需求该不该做、某些功能要不要实现感到困惑,于是决定写下这篇笔记,提醒自己应该如何进行快速的早期软件项目开发。

核心原则

  • Make it work -> Make it right -> Make it fast
    先让它工作,再让它正确,最后让它快速

  • Done is better than perfect
    完成比完美更重要

  • 协议类型/动画细节通常不是决定因素
    写下这点是因为我在过去的一些项目中过分关注了这些内容,读到这里你也可以思考一下自己是不是过分关注某些不必要的细节?

  • Always have something to show
    每个阶段都要有可演示的成果

  • 80% 的价值通常只需要 20% 的时间
    二八定律的又一次体现,或者是更加激进(或许也更加真实)的帕累托法则的体现。

以上这些原则看似简单,但在实际开发中却很容易被忽视。下面我将解读这些原则。


解读

先让它工作,再让它正确,最后让它快速

错误顺序:
一边写代码,一边优化性能,一边重构,一边加新功能,结果就是什么都没做完。

正确顺序:

  1. Step 1: 用最简单的方式实现功能(能跑就行)
  2. Step 2: 添加错误处理、测试、边界情况(保证正确)
  3. Step 3: 性能分析,优化真正的瓶颈(提升性能)

例子:

  • KVStore 先用线性查找命令(work
  • 再完善错误处理和测试(right
  • 最后考虑哈希表优化(fast

完成比完美更重要

现实情况:

  • 完成 80% 就上线的产品,可以获得用户反馈,之后迭代改进
  • 追求 100% 完美的产品,超长的开发周期导致极易错过市场机会

例子:
Redis 从未使用二进制协议,但成为最流行的 KV 存储

协议类型/动画细节通常不是决定因素

常见误区:

  • 必须用二进制协议才快(Redis 用纯文本协议也很快)
  • 动画必须用贝塞尔曲线才好看
  • 必须手写汇编才能高性能(要在非常关键的位置用汇编才能有效提高性能)

性能的真正决定因素:

  1. 架构设计:比如单线程和多线程的选择(Reactor vs 多线程
  2. 数据结构:应该选择数组、哈希表,还是红黑树?
  3. 算法:算法复杂度是指数级别、对数级别还是常量级别?
  4. I/O 模型:阻塞还是非阻塞?是不是 I/O 密集型任务?

每个阶段都要有可以演示的成果

为什么这点如此重要?

  • 可以及早发现问题
  • 对维护团队士气有帮助(在团队工作中,这点的重要性可能比你想象的还要重要)
  • 可以获得反馈
  • 证明项目的价值,或许能争取到更多的资源

建议频率:

  • MVP 阶段:每天都有新进展
  • 功能完整阶段:每周一次演示
  • 打磨阶段:每个优化都能看到效果

80% 的价值通常只需 20% 的时间

  • 前 20% 时间 → 实现 80% 核心价值 ← MVP 重点
  • 中间 60% 时间 → 完善细节到 95%功能完整
  • 最后 20% 时间 → 打磨到 100%细节打磨

注意: 很多项目在前 20% 的时间就开始追求 100% 完美,结果就是永远停留在前 20%

理解了这些核心原则后,我们需要将它们落实到具体的开发流程中。基于以上原则,我们可以将软件开发过程分为三个阶段,每个阶段都有明确的目标和判断标准。


渐进式开发三阶段

阶段一:核心可用 (MVP)
    ↓ 验证架构可行性
阶段二:功能完整
    ↓ 满足所有需求
阶段三:细节打磨
    ↓ 追求卓越体验

阶段一:最小可行产品 (MVP)

目标: 用最少的代码证明核心概念可行。

需要满足的标准:

  • 核心功能可用
  • 架构设计没有明显缺陷
  • 能进行基本的测试和调试
  • 可以演示主要功能

注意: 不要在 MVP 阶段就过分关注性能

何时进入第二阶段?

自检清单:
□ MVP 已完成并可演示
□ 架构设计经过验证,无需大改
□ 核心功能没有明显 bug
□ 团队/用户对方向达成共识

如果以上全部打勾 → 进入第二阶段
否则 → 继续完善 MVP

阶段二:功能完整

目标: 补全所有计划的功能模块

判断标准:

  • 所有用户需求都已实现
  • 系统稳定,无致命 bug
  • 通过完整的功能测试
  • 满足基本的性能要求

阶段三:细节打磨

目标: 从能用到好用

警告: 请不要跳过前面的阶段直接来到阶段三,因为在核心功能和架构稳定前进行细节打磨可能纯属浪费时间。

了解了三个阶段后,在实际开发中我们经常会遇到这样的问题:这个功能现在该不该做?这个优化要不要实现?为了帮助我们在日常开发中更好地应用这些原则和阶段划分,我们可以使用以下决策树来指导功能开发的选择。


决策树

这是一个优先考虑 MVP 阶段,但也适用于所有阶段的决策树:

开始
  ↓
[这个功能影响核心演示吗?]
  ├─ 是 → [能否快速实现(<1天)?]
  │        ├─ 是 → 立即做 ✅
  │        └─ 否 → 简化需求,快速实现 ⚠️
  └─ 否 → [当前阶段是否已完成?]
           ├─ 否 → 延后到下一阶段 📝
           └─ 是 → [性价比如何?]
                    ├─ 高 → 可以做 ✅
                    └─ 低 → 延后或不做 ❌

遇到选择时问自己:

  • 📌 现在必须做吗?
    → 不是必须就延后

  • 📌 最简单的方案是什么?
    → 从简单开始

  • 📌 能快速验证吗?
    → 快速试错,及早发现问题

  • 📌 对核心目标有帮助吗?
    → 专注主线,避免功能蔓延

  • 📌 用户真的需要吗?
    → 避免自嗨,基于反馈决策

要记住: 完成比完美更重要

决策树可以帮助我们做出理性的选择,但在实际开发中,我们还需要警惕两种常见的陷阱,它们会让我们偏离正确的开发路径。


警惕功能蔓延

常见想法:

  • “既然做了 X,不如顺便做 Y 吧”
  • “这个功能很简单,加上也不费事”
  • “用户可能会需要这个功能”
  • “我看 XXX 产品有这个功能”

如果你这样做了,结果就是:

  • → 核心功能迟迟无法完成
  • → 代码复杂度爆炸
  • → 测试和维护成本激增

完美主义陷阱

常见想法:

  • “这个代码我觉得还可以更优雅”
  • “我再重构一下这个模块”
  • “这个动画总觉得差点意思”

结果: 最后这 20% 的完美,可能要消耗 80% 的时间,最后会导致项目疯狂延期。

了解了原则、阶段和需要警惕的陷阱后,我们还需要将这些理念落实到具体的代码实践中。下面介绍几种在代码层面践行 MVP 开发的方法。


代码层面的实践

用注释标记决策和 TODO

好的注释是解释为什么,而不是是什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// NOTE: 协议类型并不是性能的决定因素,过早优化不如先解决核心问题
// 当前使用文本协议,如果未来证明是瓶颈再考虑二进制协议
int kvs_protocol_parse(char *msg) {
    // ...
}

// TODO: [MVP后] 考虑使用哈希表优化命令查找
// 当前 15 个命令,线性查找 O(n) = 15,性能足够
// 如果命令数量超过 50,再考虑优化
int kvs_parser_command(char** tokens) {
    // ...
}

// FIXME: [P2] 内存泄漏风险 - 需要在异常情况下释放 value
// 暂时可接受,因为正常流程不会触发
int kvs_array_set(kvs_array_t *inst, char *key, char *value) {
    // ...
}

功能开关:渐进式启用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// kvstore.h - 功能开关
#define ENABLE_ARRAY   1    // ✅ MVP 必须
#define ENABLE_RBTREE  0    // ⏸️ 第二阶段
#define ENABLE_HASH    0    // ⏸️ 第二阶段

// 协议优化
#define USE_BINARY_PROTOCOL  0  // ⏸️ 第三阶段
#define ENABLE_CACHE         0  // ⏸️ 第三阶段

// 性能监控
#define ENABLE_METRICS       0  // ⏸️ 第三阶段

接口先行:预留扩展空间

虽然我们强调 MVP 阶段要简单实现,但这并不意味着要牺牲架构的可扩展性。

好的设计应该做到接口稳定,实现可替换,这样在后续阶段扩展功能时就能轻松替换实现,而不需要修改调用方的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义统一接口
typedef struct kvs_engine_ops {
    int (*set)(void *inst, char *key, char *value);
    int (*get)(void *inst, char *key, char **value);
    int (*del)(void *inst, char *key);
    int (*mod)(void *inst, char *key, char *value);
    int (*exist)(void *inst, char *key);
} kvs_engine_ops_t;

// MVP 阶段:只实现数组引擎
kvs_engine_ops_t array_ops = {
    .set = kvs_array_set,
    .get = kvs_array_get,
    // ...
};

// 第二阶段:轻松添加新引擎
kvs_engine_ops_t rbtree_ops = {
    .set = kvs_rbtree_set,
    .get = kvs_rbtree_get,
    // ...
};