找回密码
立即注册
搜索
发回帖 发新帖

6290

积分

0

好友

796

主题
发表于 昨天 23:31 | 查看: 4| 回复: 0

本文译自 7 Problems Senior Developers Solve Before They Touch the Code。

收到功能需求时,写代码似乎是推进工作的最直接方式。打开相关文件、新建一个端点、添加一个数据库字段,或者搭建第一个组件,都能立刻把抽象的需求变成看得见的东西。实现一旦开始,任务就显得没那么庞大了,尽管还有几个重要问题尚未得到解答。

资深开发者通常不太愿意这样起步。他们可能会花时间阅读现有工作流,询问用户究竟想完成什么,检查旧数据的行为,或者讨论部分操作失败后应该怎么办。从外部看,这似乎比直接构建那个显而易见的方案更慢。实际上,他们正在解决一些问题——如果不提前解决,这些问题就会被代码无意中定下答案。

代码重构概念图

每个悬而未决的问题最终都会得到一个答案。如果没人决定由谁负责某条业务规则,多个层次可能各自实现不同的版本。如果没人定义成功意味着什么,一位开发者可能认为写入数据库就算完成,另一位却期待通知也必须送达。如果不考虑历史数据,一个新的必填字段可能适用于未来的记录,却让部署前创建的所有记录都无法正常使用。

资深开发者知道,第一次实现有着非同寻常的影响力。它的假设会变成数据库结构、API 契约、共享辅助函数、状态模型和测试。一旦这些决策有了调用方和生产数据,修正它们的成本就会远高于一开始讨论清楚的成本。以下是经验丰富的工程师在动手写代码之前,通常会先尝试解决的七个问题。

1. 弄清所提功能与实际问题之间的差别

功能需求往往是一个预先提出的解决方案,写法却仿佛问题已经被充分理解了。有人要求增加一个按钮、新的筛选条件、导出选项,或额外的状态。这个需求可能完全合理,但它描述的是某个人设想应该构建什么,而不是促成这一需求的困扰、风险或业务目标。

导出功能隐藏的真实问题

资深开发者会先找出背后的问题,再决定是否采用所建议的实现方案。要求增加导出按钮,可能是因为管理者无法比较各部门的每周表现。构建 CSV 导出功能或许有帮助,但也可能引入手动操作,而保存好的报表或定时汇总反而能更直接地解决问题。要求增加一个状态,可能暴露出同一个状态字段被迫同时表示付款、审批和履约情况。添加这个值也许能满足眼前的工单,却会让模型更难理解。

这并不意味着工程师应该质疑每个需求,或者把小任务变成产品研讨会。目标是充分了解用户的处境,从而判断所提改动能否改善真实的结果。几个具体的问题,就能避免花上几周时间打磨错误的方案——现在是谁无法完成工作?他们正在手动做什么?新增的信息会帮助他们作出哪项决策?有了这个功能后,哪些事情会成为可能?

如果这个问题没有被解决,开发者自然会围绕表面需求进行优化。他们把按钮实现得很整洁,设计出可复用组件,并围绕某种行为构建精良的 API,但这种行为可能并不能解决最初的困难。代码在技术上可以很出色,功能却仍然令人失望,因为实现只是精确地满足了所提要求,而没有充分回应背后的问题。

资深开发者并不是想推迟写代码。他们是在确保代码确实是正确的应对方式。

2. 定义成功的含义

许多需求描述了一个动作,却没有定义何时才算完成。用户提交申请,管理员审批记录,客户复制项目,或定时流程同步数据。正常流程听起来很清楚,直到它涉及多项操作,而其中一项失败。

审批工作流成功标准

设想一个审批工作流,需要更新数据库、写入审计记录、发布事件并发送通知。如果数据库更新成功,但通知发送失败,审批仍然算成功吗?用户应该重试整个操作吗?如果重试,会不会审批两次?如果审计记录写入失败,是否应该回滚状态变更?如果事件延迟了,操作是已经完成,还是仍在等待处理?

这些并不是实现细节。它们定义了产品作出的承诺。

资深开发者会在编写工作流之前,先明确完成的边界。有些操作必须一起成功,因为部分完成会导致无效状态。这些操作可能应该放在同一个 事务 中。另一些操作虽然有用,却不应决定主要动作是否成功。邮件延迟不一定应该导致账户创建失败。在这种情况下,系统可能需要后台流程、重试策略,或者一条明确标记通知仍待处理的记录。

同样的问题也适用于更简单的功能。如果用户启动导出,成功意味着文件立即生成、任务已被接受,还是下载链接最终会出现?如果界面显示“已保存”,这意味着服务器确认了持久化,还是仅仅意味着本地状态发生了变化?

如果成功的含义模糊不清,不同层就会自行定义。后端仍在处理时,界面可能已报告完成。后台任务处理程序可能重试一个用户已经认为失败的动作。监控系统可能认为请求成功了,但某个必不可少的后续操作却没有执行。

资深开发者会尽早解决这个问题,因为成功的含义会影响状态模型、API 响应、事务、测试、重试和与用户的沟通。一旦明确了系统承诺,实现通常就会更简单,因为每一步都有了明确的作用。

3. 明确谁有权作出决策

如果正确的决策是在错误的位置作出的,一个功能即使行为正确,在架构上仍可能不安全。前端为无权限用户隐藏了按钮,但直接调用 API 时,请求仍会被接受。控制器检查状态转换是否有效,但定时任务绕过控制器调用了服务。服务检查重复数据,但另一个脚本直接写入数据库。

权限决策归属问题

规则虽然存在,却不是每条路径都被迫遵守它。

资深开发者会寻找拥有足够权威、能够负责这项决策的边界。界面可以反映权限,以改善易用性,但它不能充当安全边界。控制器可以验证 HTTP 请求的结构,但业务规则可能需要放在技术栈的更深层,让后台任务和内部调用方也得到相同的行为。应用可以先进行友好的唯一性检查,而数据库仍然负责在并发写入时保护不变量。

这个问题也适用于状态归属。如果筛选条件必须能够分享,并且在刷新后保留,那么 URL 可能是合适的归属位置。如果某个值只是临时的表单输入,通常应该由表单管理。如果权限和持久化状态来自服务器,前端就不应根据本地假设,悄悄推导出另一套版本。

权责不清会催生大量用于协调的代码。多个副本需要同步,多个层重复相似的检查,未来每次改动都变成了到处搜索。新增一项策略,需要更新组件、控制器、服务、后台任务处理程序和查询,却无法保证各个版本仍然表达相同的含义。

资深开发者会有意识地为每项重要决策指定负责人。其他层可以展示、执行、缓存或传递结果,但不应独立地重新定义它。这样既能降低安全风险,也能减少维护成本,因为团队知道权威定义从哪里开始。

4. 解决历史数据问题

新代码很自然地会按照开发者想要的模型编写。生产数据却反映着应用曾经使用过的每一种模型。

历史数据默认值困境

要求某个字段必填,对部署后创建的记录来说可能很简单,但数千条已有记录可能并没有这个值。新的状态模型可能准确描述了未来的工作流,却让历史记录停留在已经不再存在的状态中。新的关联关系可能是必需的,但旧数据行是在相关实体出现之前创建的。

资深开发者会在添加校验或修改数据库结构之前考虑这些情况。他们会问:旧记录能否在不歪曲事实的前提下迁移?是否需要一个临时的未知状态?这个功能是在重新定义历史,还是只改变未来的行为?

“不歪曲事实”很重要。用一个方便的默认值填补所有缺失值,可能满足了数据库约束,却制造了虚假信息。如果系统不知道某条旧记录曾归哪个部门所有,把它分配给当前部门也许能让查询更容易,却会降低历史报表的可信度。有时,正确的模型应该包含“未知”“未采集”或某个历史遗留类别,因为这种不确定性本身就是数据真实历史的一部分。

历史兼容性也会影响 API 和集成。旧客户端可能仍然发送旧格式的字段。定时任务可能依赖新工作流准备移除的状态值。报表可能仍按多年前已经变更的规则解读字段。

经验丰富的开发者不会试图永远支持所有历史行为。他们会明确作出迁移决策。有些数据可以安全转换,有些需要人工审核。有些旧客户端需要临时的兼容期,另一些则可以有计划地停止支持。

忽略这个问题并不能消除它,只会把复杂性转移到分散各处的兜底逻辑、可空字段、失败的迁移,以及部署后的支持事件中。尽早解决它,能让团队完成一次统一、连贯的过渡,而不是让各处分别猜测旧数据究竟应该代表什么。

5. 考虑重复执行与并发操作的可能性

大多数功能描述都假定只有一个操作者,而且执行顺序清晰。用户只点击一次,请求只到达一次,操作完成前记录保持不变,响应也会顺利到达客户端。

并发操作资源竞争

生产系统几乎无法保证其中任何一个条件。

页面看起来卡住时,用户可能再次点击。移动网络可能在服务器完成请求后、响应到达前中断。队列可能把同一条消息投递两次。两位管理员可能打开同一条待处理记录,并在相隔几秒的时间内批准它。用户仍在查看旧版本时,定时流程可能已经更新了数据。

资深开发者会在选择实现方案之前,先判断操作在重复执行和 并发 访问下是否安全。如果重复执行没有危害,系统可能不需要额外保护。如果重复执行会产生另一笔付款、另一份邀请、另一个文件或另一条库存预留,工作流就需要一种机制,识别出多次尝试实际上代表同一个逻辑动作。

这种保护可以来自幂等键、唯一约束、条件更新、版本列、事务,或者已处理消息记录。正确的机制取决于风险所在的位置。重要的是,应用不能依赖“我们事先检查过了”,仿佛写入前不会有其他进程采取行动。

并发尤其危险,因为单独审查时,每一行代码都可能看起来正确。问题出现在读取与更新之间。两个请求看到相同的有效状态,都通过了校验,然后都写入了本应只发生一次的结果。

资深开发者会尽早解决这个问题,因为数据损坏的代价远高于一次明确可见的冲突。他们宁愿让数据库拒绝其中一个竞争操作,也不愿让系统存下两个互相矛盾的事实,之后再靠人工修复。

6. 考虑部署后如何理解功能的运行情况

本地开发提供了生产环境所没有的上下文。开发者可以设置断点、检查数值、重复操作,也清楚记得功能是如何设计的。部署后,团队可能只会收到一条支持消息,说某件事失败了。

分布式请求链路故障定位

资深开发者会在实现之前设想未来的排查过程。如果操作没有完成,别人如何知道哪个阶段失败了?能否跨服务追踪同一个请求?日志能否显示该动作尝试了一次还是多次?团队能否区分从未开始、仍在处理、部分完成和彻底失败?

这并不要求在应用中到处添加日志。缺少结构时,更多输出反而会让排查更困难。有用的 可观测性 会记录重建一次重要操作所需的少量事实,例如请求标识符、资源 ID、操作名称、耗时、状态转换、尝试次数,以及稳定的错误类别。

系统还需要保留失败本身的含义。查询失败不应该悄悄变成一个空集合。服务提供方超时,不应该让“操作可能已经在远端完成”这一信息丢失。一个大范围捕获异常的代码块,不应该在错误抵达最终日志记录边界之前,把所有原因都替换成一句泛泛的描述。

资深开发者还会考虑支持与恢复。失败的任务能否安全重试?支持人员能否查看任务的当前状态,而不必手动查询好几张表?用户能否再次尝试,而不产生重复工作?是否有足够的信息,能告诉用户究竟发生了什么?

一个无法解释自身运行情况的功能,在第一次出现意外行为时就会变得昂贵。事后再添加可观测性可能已经太晚,因为有用的上下文只在操作运行期间存在。经验丰富的开发者会在编码前解决这个问题,让证据成为设计的一部分,而不是事故发生后的紧急补丁。

7. 明确团队将如何证明改动是安全的

测试往往在实现之后才被讨论,因此测试自然会沿着代码当前的结构编写。引入了一个辅助函数,测试就验证它是否被调用。添加了一段兜底逻辑,测试就把它固定下来。模拟了一个仓储对象,测试就确认它返回了事先为模拟对象设定的值。

从开发到生产的发布风险

资深开发者会在实现之前思考如何验证,因为验证要求本身可能暴露设计中的弱点。如果操作必须能够安全重复,测试就应该多次执行它。如果两位用户不能并发应用同一项状态转换,测试就需要制造竞争更新。如果失败和不存在是不同的结果,就必须能从契约中观察到这两种情况。如果某条规则由数据库约束保护,使用模拟对象的 单元测试 就无法证明它。

这并不意味着每个功能都需要庞大的集成测试或端到端测试套件。合适的测试层级取决于系统保证由哪一层负责。纯粹的数据转换可以直接测试。API 契约可能需要集成测试。数据库不变量需要验证真实的数据库行为。涉及高风险迁移的发布,可能需要监控、分阶段部署,或带有明确移除计划的功能开关。

经验丰富的开发者还会考虑可逆性。如果改动引发了意外问题,能否在不丢失新写入数据的情况下禁用或回滚?迁移是否支持安全过渡,还是数据库结构变化后,旧版应用就会失败?能否先向较小的用户群体逐步发布,而不是让所有人立刻依赖它?

这些问题会在代码让调整变得昂贵之前,影响实现方式。一个难以测试或回退的设计,可能在单个操作中隐藏了过多职责。一个无法安全发布的改动,可能需要先迈出更小的中间一步。

资深开发者并不追求绝对的证明。软件始终存在不确定性。他们会尽量让最重要的保证可被观察和验证,让风险最高的决策可以撤回,从而让团队在不造成永久损害的前提下学习。

问题变小之后,代码通常更容易写

资深开发者在编码前做的工作,看起来可能像是在犹豫,因为这些工作不会立即产出文件、函数或拉取请求。他们正在澄清用户的真实问题、定义成功、明确权责、直面历史数据、考虑并发执行、设计运行证据,并决定如何验证改动。

这些讨论有价值,并不是因为规划天然比行动更好。过度分析会拖延简单的工作,还会为不重要的风险设计架构。经验丰富的工程师通常只会着重识别那些决策:如果代码给出了错误答案,纠正它们的成本会很高。

一旦这些问题得到缓解,实现往往会变得出乎意料地直接。状态模型中的无效组合更少了,业务规则有了唯一的负责人,数据库保护着不变量,响应能说明工作已经完成还是仅仅被接受,测试关注的是系统承诺而不是当前函数的组织方式。

这也是资深开发者在起步时可能显得更慢,却能更快完成整个任务的原因之一。他们并不是在回避实现,而是在防止实现成为这样一个地方:产品需求中的模糊之处、数据历史、并发问题和运行中的不确定性,在这里被悄悄转化为永久的技术决策。

解决这些问题的最佳时机,是第一个方便的方案开始拥有调用方、测试和生产数据之前。

这就是经验丰富的工程师在动手写代码前,往往会花更多时间思考的原因。他们知道,写出函数通常是容易的部分。难的是决定这个函数究竟应该承载什么含义。如果你也在遭遇类似的决策困境,不妨到 云栈社区 看看同行们的讨论。




上一篇:treg 2.7K Star:Agent 工具版 OpenRouter,按次调用 3000+ 接口不买订阅
下一篇:SQL NULL 为什么不能用等号判断?三值逻辑与 NOT IN 陷阱解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-5 01:37 , Processed in 0.064040 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表