设计切实可行的软件

13 阅读8分钟

设计切实可行的软件

原始链接: www.seangoedecke.com/planning-so…

当有人向我描述一款软件时,我总会思考自己会如何构建它。很多软件工程师也会这样做,但往往做得不好。我经常看到大家为一个总体计划中根本行不通的具体细节争论不休。比如,纠结前端是用 prop-drilling 还是 context-passing 来传递一个我们根本拿不到的数据,或者在一个必须保持无状态的后端服务中讨论持久化数据的存储策略。

为了避免这种情况,我建议在脑海中从头到尾推演一个重要的用户流程。具体该怎么做呢?

两个常见的反面模式

第一种常见错误是过于宏观。假设你要给博客做个评论系统,有些工程师会想:“哦,把评论存到关系型数据库里,然后在页面上读取出来就行了。” 关系型数据库或许是个对的选择,但这层面的设计对实际开发毫无帮助。你需要深入一层:评论到底是怎么从用户的浏览器传到关系型数据库的?

第二种错误是过早陷入无关的细节。有些工程师一上来就说:“太棒了,我要用 React”,然后开始纠结无数微观决策:要不要用 RSC?用 fetch 还是 TSQ 获取数据?要不要用 GraphQL 暴露评论数据?刚开始面对问题时,绝对不是做这些决定的时机。你最终可能需要决定,但绝不是在最开始。

正确的方法

那么正确的方法是什么?就是挑出最重要的用户流程,然后在脑海中把最简单的实现方式从头到尾推演一遍。你一次只能推演一个流程,否则脑子会乱1;你必须从头到尾贯穿始终,否则会遗漏关键细节。这里的“推演”,指的是“伪代码”级别的思考:你不必在脑海中写出所有代码,但必须想清楚每一个逻辑步骤。

这就像《程序员修炼之道》中著名的“曳光弹”规则2:你的第一个原型应该是能让一个用户流程跑通的最小工作量。构思软件也是同样的道理:你的目标是在脑海中把一个用户流程从头到尾跑通。

这么做的好处是,它逼迫你面对真正重要的问题(就像你用真实代码写原型时一样)。你不需要设计出最干净、最完美的方案,但你必须设计出切实可行的东西。从行得通的方案开始,你通常能迭代出优秀的方案;但如果从行不通的方案开始,你很难再绕回到可行方案的空间里。

案例演练

以实现评论系统为例:

用户来到我的博客文章页面,应该看到一个“输入评论”的表单。这很简单,在文章模板里加个 <form> 元素就行了。

当他们提交表单时,评论需要存起来。好的,所以我需要一个后端接口和某种数据存储。我的“添加评论”接口代码大概长这样:

comment = params['comment_body']
post = params['post_id']
user = ???

Comment.new(comment: comment, post: post, user: user).save!

redirect_to(post)

我该怎么设置评论的用户?如果是匿名评论,很好办(加个选填的 name 字段就行),否则我就得在我原本纯静态的网站上加上登录功能。这个接口跑得慢一点没关系,因为用户写评论的频率远低于浏览页面,花个一秒钟无伤大雅。

重定向后,用户应该能看到自己的评论。这意味着我需要在文章页面上加点逻辑:把每个评论用 HTML 模板渲染出来。这个读取接口必须非常快,因为看文章是网站的核心操作,增加几百毫秒的延迟会严重影响体验。这就意味着将来可能需要缓存或延迟加载,并且评论多了还得做分页。

即使是这么简单的例子,你也能看出来它暴露了我缺失的基础设施(运行后端代码的能力、数据存储),以及需要解决的问题(用户如何登录或确认身份)。当你在大型科技公司的系统中做同样的推演时,往往会发现更多有趣的问题:

  • 我们的系统需要数据 X,但它只能从服务 Y 的一个慢接口获取。
  • 我们需要一些目前没收集的数据(比如我的静态博客没收集用户身份信息)。
  • 每次有新评论我们要展示出来,但文章目前被 CDN 长期缓存了,我们得想办法在有新评论时刷新缓存。
  • 我们需要考虑恶性功能 (wicked features) —— 比如,如果有人用的是私有部署版博客,我们得弄清楚评论该存在哪(或者干脆隐藏/禁用评论表单)。

注意,以上所有推断都与具体的技术选型无关(不管后端用 Rails 还是 Express,数据库用 MySQL 还是 MongoDB)。它们是基于需求的普遍假设:无论如何,评论总得存在某个地方,并以某种方式与用户关联。

如何沟通你的脑内计划

这个计划主要应该留在你脑子里。根据我的经验,你很难向产品经理甚至其他工程师解释清楚它的用处。这个计划的真正价值在于帮你评估工作量提出关键问题。比如,给我的博客加评论,最难的部分是把纯静态架构改成动态后端(能存数据、能运行代码)。评估这部分的工作量就能给整个项目提供一个粗略的指导,而随之而来的问题(比如,我的博客该迁移到什么平台,或者干脆用第三方的评论服务?)将是项目规划中最核心的问题。

初步沟通结束后,把计划写下来会很有用。我喜欢用松散的框线图(通常是 Mermaid 图),但形式不重要,写一小段文字可能就足够了。书面计划可以作为讨论具体实现的绝佳起点。如果团队一致认为评论应该由有状态的后端应用来管理,那我们就可以开始讨论该用什么技术、怎么用了。

就算你们团队有更正式的设计流程(比如设计评审会,或架构师主导的会议),这种方法依然有效。如果你带着一个“功能可以如何运转”的具体想法去开会,成功的概率会大得多。唯一要注意的是:不要过于执着于你的想法3。你要保持开放的心态,随时准备大幅修改计划,只要新方案同样行得通就行。你脑海中冒出的第一个粗略想法,通常不会是最终的最优解。

总结

这似乎是个不言自明的道理:规划工作时,你当然应该考虑系统到底要怎么运作。但我认为,很多工程师低估了“让系统真正跑起来”的难度,从而忽略了他们最该关注的实际细节。

如果你能立刻跳入这些实际细节中——有什么数据可用、数据要去哪、怎么传过去——你就能少浪费大把时间,不再为那些从一开始就注定行不通的方案争论不休。


如果你喜欢这篇文章,可以考虑订阅我的邮件推送,或者[在 Hacker News 上分享它](news.ycombinator.com/submitlink?… software that could possibly work)。下面是一篇相关文章的预览:

恶性功能 (Wicked features)

为什么在大型科技公司工作这么难?

因为一小部分“恶性功能”主导了其他所有事情。如果你在做一个待办事项 App,给待办加图片可能是个大功能,但它不是恶性功能。然而,要求你的 App 同时支持网页版和独立客户端,这就是个恶性功能。区别在哪?恶性功能是那种每次你开发任何其他功能时,都必须考虑进去的功能
继续阅读...


Footnotes

  1. 好吧,也许你骨骼惊奇能同时推演好几个——反正我只能推演一个。即便如此,我也建议你专注于一个,把多余的脑力用在深入推演实现细节上。

  2. “曳光弹”规则得名于现实中的曳光弹,它能让机枪手在夜间更容易瞄准。他们不需要在开第一枪时就精确计算子弹的落点,而是可以先开火,然后根据发光子弹的轨迹进行调整。

  3. 如果只有你一个人开发,那就把这句话理解为“如果我确信新方案更好的话”。