标签: 工程师方法论

  • 写代码和做工程有什么区别:局部实现与系统责任

    作者:lihao技术文章发布:工程师方法论 · 第 02 期

    从一次格式转换扩展到可维护交付,说明错误传播、可观测性、版本、发布与恢复为何必须进入工程完成标准。

    函数返回正确,任务就结束了吗

    一个转换函数在正常样例上输出正确,但程序把结果写到一半时磁盘满了,下次运行又误把半成品当成完整文件。函数的局部计算可能没有问题,用户却已经失去可信的结果。

    写代码是实现行为的重要手段;做工程还要承担该行为进入完整环境后的责任。二者不是职业高低之分,也不是说程序员不做工程,而是看讨论范围:从局部输入输出,扩展到运行、交付、变化和失败。

    本篇延续上一篇的假设性文件转换任务,所有事故情境都是构造案例,没有虚构真实项目经验。

    先画出完整路径,再决定代码边界

    用户选择输入→解析→转换→校验→写入→通知完成。每一段都有接口,数据、错误和资源的责任必须沿路径传递。解析器拒绝未知编码,调用方就不能把拒绝当成空数据继续写“成功”。

    局部函数可以承诺合法记录转成合法记录;系统还要保证用对版本、实际读到全部输入、输出未损坏。单元测试覆盖局部性质,集成与端到端检查覆盖连接后的行为;任何一层都不能自动替代另一层。

    NASA关于产品实现的讨论把验证与最终用途确认分开,也强调集成与转移过程。[1] 这里借用的是责任分解方法,不把一个小工具冒充航天工程的复杂程度。

    失败发生时,结果应该处于什么状态

    一个候选设计是先写临时产物,校验通过后再提交为正式结果。若写入失败,保留原输入,标记失败并避免半成品被误读。实现时还要检查文件系统与平台语义:单个文件重命名的保证不能直接推广为整个目录树的事务。

    重试也不是“再跑一次”那么简单。若操作会发送通知或覆盖已有输出,必须考虑重复副作用;若恢复点记录不可靠,断点续跑可能跳过未完成的记录。应明确一次任务的身份、已完成范围与重复运行的规则。

    这是一份设计评审候选,不是已实现并验证的恢复机制。停止条件包括:无法确认输出完整性、输入版本发生变化、校验规则不兼容;此时应停止提交,而不是用成功提示遮住不确定状态。

    可观测性,是让故障变得可定位

    只写“转换失败”通常不够。可以记录任务版本、输入标识、处理阶段、已完成数量与错误类别;涉及私密文本时,不应为了诊断把完整内容写进公开日志。诊断信息也有最小化与权限边界。

    用户需要知道结果是否完成,维护者需要知道在哪里失败。两者的输出可以不同:界面给出明确下一步,内部日志保留定位所需细节。暴露堆栈不是完整用户体验,隐藏所有原因也不是稳健设计。

    指标同样要和目标对应。总耗时、失败比例、峰值内存可能有用;若只记录平均值,少数严重卡顿就可能被掩盖。选指标的目的是检验承诺,而不是装满一个仪表盘。

    修改与发布会创造新的责任

    格式从v1变成v2时,要知道旧输入能否继续读取、新输出由谁消费、是否需要迁移、怎样验证兼容和回退。代码在开发机运行,不代表依赖、权限与配置在部署环境都满足。

    Google SRE的Release Engineering章节讨论可重复构建、发布流程与配置管理。[2] 对本例可缩减为:记录源码版本与依赖;用固定样例核对产物;先在隔离输出目录运行;保留可重新执行的上一版本与原输入。

    回退也有边界:旧程序未必能读取新版本已经生成的数据。若迁移不可逆,回退代码本身不够,还需经过验证的数据恢复方案。不能写下“有回滚”三个字,就宣称恢复已验收。

    把错误处理写成状态机,遗漏就更容易看见

    为转换任务定义Created、Running、Validated、Committed与Failed五个状态。Created尚未处理,Running可能有临时输出,Validated表示约定检查已通过,Committed才允许把结果对外标为完成,Failed记录失败原因与可恢复范围。

    正常路径是Created→Running→Validated→Committed。解析或写入错误可从Running进入Failed;校验错误可从Validated之前进入Failed。若还没校验就直接写“完成”,系统绕过了完成条件;若进入Failed后仍暴露临时目录为正式结果,错误状态就没有隔离副作用。

    状态机不要求必须引入复杂框架。一份清晰枚举与几条转换规则就可能足够。重要的是每条转换的守卫条件与持久化时机:程序崩溃后,仅凭内存里的“已校验”标志,下一次运行未必能安全恢复。

    例如先把临时文件替换成正式结果,再写入 Committed 记录。若替换已经发生、记录写入前进程崩溃,重启后只看状态记录会把这次任务误判为未提交;盲目重跑又可能重复通知或覆盖结果。把顺序倒过来也不自动正确:若先写 Committed、再替换失败,就会宣称一个并不存在的结果已完成。

    设计必须规定正式产物、任务身份与完成记录之间怎样对应,以及恢复时怎样核对并消除这个不确定窗口。这里指出的是协议责任,不提供适用于所有文件系统和外部副作用的原子提交实现。

    它也帮助定义不变量:原件始终不被写入;只有Committed结果可供下游消费;相同任务身份不能同时产生两个互相矛盾的正式结果。验证可以围绕这些性质设计,而不只是逐个函数覆盖。

    重试、幂等与“恰好一次”不是同一个承诺

    如果某个步骤执行完,但确认消息丢失,调用方无法仅凭超时判断它没发生。直接重试可能重复写文件、重复记账或重复发送通知。超时描述的是等待结果的状态,不能自动推出远端没有副作用。

    对本例,可以给输入清单、转换版本与参数形成任务身份,并把正式输出与该身份绑定。相同任务再次到来时,若已经存在可验证的完成记录,就返回已有结果;若只有临时产物,就按恢复策略处理。输入内容改变而路径相同,不能误用旧结果。

    幂等表示重复操作在规定语义下不会增加新的效果;它依赖具体操作与状态,不是接口名字里加一个request_id就成立。“恰好一次”若覆盖多个外部系统,还需要讨论原子性与恢复边界,本篇不靠一个本地示例承诺它。

    这种分析说明工程责任为什么会超出局部实现:函数可能正确完成一次调用,而完整协议仍然在重复、丢失与中断时产生错误。

    从补丁到可交接版本,还应留下什么

    交付记录可以保持简短,但至少包括:源码与构建身份、依赖与配置、输入输出格式、支持范围、启动方式、验收证据、失败处理及回退限制。任何一项若只存在于开发者脑中,接手者都要重新支付发现成本。

    变更评审还要看兼容方向。新版本能读旧输入,是向后读取兼容;旧版本能否读新输出,是另一件事。只升级程序而改变持久化格式,可能让回退失败。发布时需要说明迁移是否可逆,以及旧数据和配置在哪里保留。

    有些证据可自动取得,有些必须人工确认。自动化适合重复、明确的性质;真实使用目的、可理解的诊断与组织责任仍需要评审。测试通过率不能代替完整验收,也不能因不能自动化就放弃记录。

    工程完成不等于永不失败,而是当前承诺有依据、失败可被识别、变化有路径。对一次性任务可缩减流程,但不能把不可逆风险留给用户在故障时才发现。

    可靠性投入,也需要停止条件

    给一个一次性、可重建的只读转换任务引入多机调度、复杂事务与完整监控平台,可能比原问题更难维护。工程责任不是无限增加防护,而是识别失败的概率、影响与恢复成本,再选择足够的措施。

    可以先列故障模式:输入非法、磁盘满、进程中断、版本不匹配、重复运行。对每一项问用户会失去什么、能否重新运行、能否检测错误、恢复需要多久。若原件保留且输出可重建,临时输出隔离与清晰失败报告可能已经承担主要风险。

    若任务进入多人长期使用、写入不可逆外部状态或承担更高影响,再提高审计、隔离与恢复证据。停止条件可以是:剩余风险已被明确接受,新增机制不再降低主要损失,或维护成本超过可避免损失。没有实际概率与成本数据时,应说明这是定性判断,不编造精确ROI。

    因此,写代码与做工程的差异不在文档厚度,而在是否对实际使用后果负责。一个很短的实现也能有清楚契约;一个流程繁多的项目也可能仍没回答失败由谁发现、谁来恢复。

    怎样防止工程变成无止境加流程

    最强异议是:这些事项会不会让一个小工具迟迟不能交付?答案取决于风险。一次性、可重建、原件只读的任务可以保持简单;长期服务、多人使用或不可逆写入需要更严格的证据。流程应随影响范围调整。

    一个合适的完成标准是:正常路径满足需求;关键失败有明确状态;版本与运行方式可重现;接手者知道支持边界;恢复方案与未验证项分别列清。若某项不适用,就说明理由,不机械补表。

    工程责任的目标是降低使用与变化的总成本。写出功能让工具开始有用,把条件、证据与恢复说清,才让它更可能长期可用。

    来源、证据与阅读路线

    1. NASA Systems Engineering Handbook,第5章,集成、验证、确认与移交
    2. Google Site Reliability Engineering,Release Engineering,构建、版本与发布流程

    证据范围:任务状态、重试与恢复场景为构造的设计分析;尚未实现所述转换工具,也未验证文件系统持久化、并发或故障恢复行为。

    版本1.2 · 2026-10-18:完成事实、条件与引用复核,逐篇明确证据方法;有实质缺口处补齐推导、反例或原题。原网址与首次发布日期保留。

  • 工程师到底在解决什么:需求、约束、权衡与可验证结果

    作者:lihao技术文章发布:工程师方法论 · 第 01 期

    以一个明确标为假设的文件转换任务,把模糊愿望变成输入输出、约束、验收和回退条件,展示工程决策怎样留下依据。

    “把它做快一点”还不是完整问题

    有人提出:做一个工具,把一批文本文件转换成另一种格式,越快越好。立即写一个循环当然可以开始,但我们还不知道:允许哪些输入,失败后原文件怎样处理,输出如何判定正确,速度是首个结果时间还是整批完成时间。

    工程师要把需求放到现实约束里,构造并交付可验证的结果。实现算法是其中一部分,界定目标、识别风险、比较方案、验证和维护也属于工作。NASA系统工程手册把需求、设计、实现、验证与确认组织为相关过程,可作为方法参考。[1]

    下面全部是构造案例,不是本人或任何企业的项目经历;数字是为说明决策而设定的假设,不是实测。

    先写任务约定的骨架

    假设需求是:把UTF-8文本中的某种字段结构转换为新格式。输入为一个明确文件清单,总量上限100MB;输出写入新目录,默认不改原件;转换失败必须报告文件、原因和已完成范围。

    “保留信息”也要具体:合法输入的字段值与顺序应按约定保留;未知字段不得静默丢弃;重复键、无效编码和过长记录要有明确处理。空文件应得到空结果还是错误,需要事先决定。

    验收不能只写“运行成功”。可以列出普通记录、空输入、边界长度、重复键、非法编码、磁盘写入失败等案例,规定每类预期。需求中的每一项重要承诺,应能对应到一个检查或明确的人工评审。

    约束和目标不要放在同一张模糊分数表里

    约束是方案必须满足的条件,例如不得覆盖原件、必须能识别不支持的编码;优化目标是在合法方案之间比较的指标,例如处理耗时和内存占用。不能用“快10%”抵消“偶尔悄悄丢数据”。

    若提出“1秒内完成”,还要说明输入大小、机器、是否包含读写、冷缓存或热缓存,以及多少次运行满足。否则不同测试条件下的数字没有可比性。对于本案例,我们只拟定评估方法,不声称已经达到某个耗时。

    NASA手册区分verification与validation:前者检查产品是否满足规定要求,后者关注产品能否实现预期用途。[2] 若用户真正需要增量处理,而我们把一次性批量转换做得完美,也可能通过了若干测试却没解决需求。

    权衡必须放在同一任务与基线上

    候选A一次把全部文件读入内存,实现较简单;候选B按记录流式处理,能限制峰值内存,但错误恢复和跨记录状态可能更复杂。先检查两者是否都满足语义,再在同一输入、配置和正确性要求下比较。

    若总量与内存预算很小,A可能足够;若最大输入无法装入内存,A就不是合法候选,不能继续拿平均速度争胜。若B让定位错误更困难,就要把诊断与维护成本一并记录。

    最强反例是:需求随时会变,何必先写?回应不是冻结一切,而是保留最小可修改约定。没有约定也会发生选择,只是选择被藏在代码和个人记忆里;一旦变化,很难知道哪些行为属于承诺。

    一份真正能复用的决策记录

    字段 本例填写方式
    目标与用户 谁用转换结果,保留哪些字段
    约束 原件只读、输入上限、错误可定位
    候选与基线 整批读取、流式读取;同一语义
    证据 样例、负例、资源测量;未做则留空
    停止与回退 输出校验失败不替换既有结果
    后续责任 版本、维护者、已知不支持项

    这张表不是要求每个小任务写厚报告,而是让关键承诺与证据有位置。对低风险、一次性工具,几行记录可能足够;对长期服务或安全关键系统,证据深度需要提高。

    把模糊需求改写成可以否定的承诺

    对前面的转换工具,先写三条具体要求。R1:在合法输入上,输出记录数、字段值与顺序符合映射规则。R2:处理过程中不改动输入文件。R3:遇到非法编码或不支持结构时,任务报告失败,不能留下被标为成功的正式结果。

    分别设计证据:R1使用独立预期文件或不同实现作对照;R2以只读方式打开输入,检查文件及所在目录的写入权限、全部写入路径和写入尝试;R3注入非法输入与写入失败,检查退出状态和输出目录。运行前后哈希在正确读取同一份完整输入、检查窗口内无并发修改、哈希实现正确且不考虑碰撞的假设下,只能支持两个检查时刻的字节一致,不能单独证明处理中从未改动。只读打开一个句柄也不能阻止经其他句柄或目录替换写入,因此保护承诺还要结合权限与运行条件。单一“进程退出0”不能覆盖三条要求。

    NASA的需求编写指南要求语句清晰、可核验,并避免fast、easy等没有操作定义的词。[3] 对本例而言,“快”可以先作为愿望保留,直到明确负载、机器与时延指标;不应随手填一个没有用户依据的秒数,制造虚假的精确性。

    还要区分需要与设计限制。用户需要不丢数据,不一定需要某一种数据库或队列;若过早把偏爱的实现写进需求,会排除其他可行方案。只有外部兼容、安全或环境确实要求时,才把具体技术设为硬约束。

    比较方案,先检查可行域,再讨论最优

    假设内存预算是64MB,最大输入100MB。整批读取方案如果确实需要把完整输入和输出同时驻留,峰值可能超过预算,就应先判为不满足约束;不能用平均输入只有1MB来绕过已经承诺的上限。

    流式方案也不自动可行。若转换需要全局排序,单条记录处理后立即写出可能不满足输出顺序;若存在跨记录引用,就可能需要额外索引或多遍处理。可行性来自任务语义与资源条件共同满足,不来自“流式更先进”的标签。

    在合法方案中,可以比较耗时、内存、实现复杂度与维护成本。若A比B快但占更多内存,且二者都在预算内,通常不能不交代偏好就宣布A全面更优。应先给出决策原则,例如优先满足交互时延,再在剩余方案中降低维护成本。

    如果没有一个方案同时满足全部硬要求,应回到需求方暴露冲突,讨论缩小输入、调整时限或增加资源。不能在实现中偷偷降低要求,交付后再用“工程总要权衡”解释。

    一个完整的失败—修订闭环

    假设初版采用逐文件写输出,正常样例全部正确,却在第37个文件遇到非法编码,前36个已写入。若需求只承诺每文件独立提交,这可能是合法部分成功;若需求承诺整批要么全有要么全无,就是缺陷。错误性质首先由契约决定。

    修订可以选择整批暂存后统一提交,也可以把用户可接受的“部分成功”明确纳入需求。前者增加临时空间与提交复杂度,后者增加用户处理未完成文件的负担。两者不是纯代码风格之争,而是不同使用承诺。

    记录应写明:触发输入、受影响输出、原要求、原因、选择、回归用例、恢复办法。若此前已交付错误结果,还要确定哪些使用者或产物受影响;把补丁合入并不自动修复历史数据。

    这个闭环的价值在于留下可追溯的判断。后来的维护者能看出为什么选择某种行为,也能在约束变化时重新评估,而不必从零猜测原设计动机。

    验收矩阵要填具体结果,不能只填“有测试”

    对R1的字段映射,写清输入含哪些字段,预期输出是什么,采用人工审查还是独立实现比较;对R2的原件保护,列出正常、失败和中断三个路径;对R3的错误报告,列出错误码、失败文件标识与是否存在正式输出。

    证据表应允许“未验证”这一状态。只有测试文件存在,还没有说明它被执行;只有执行日志,也要确认实际输入、断言与被测版本。覆盖率可以帮助发现空白,却不能证明断言本身正确或需求没有遗漏。

    若一条要求无法设计任何检验,先检查它是否含糊、不可达或层级不对。有些用途确认适合用户试用或评审,不必强行写成单元测试;但仍要明确参与者、问题、观察与结论,而不是留一个笼统的“体验通过”。

    验收的最终输出应是承诺与证据的对应关系。它让未完成项可见,也使下一次需求变化能定位受影响检查。指标越多不必越可靠,关键是重要承诺有没有独立、适当的支撑。

    交付结果,也交付判断的边界

    本例尚未实现工具,所以可交付的是需求与验收模板,不能把它登记为已通过测试的产品。真实实施时应保留输入样例、运行命令、原始结果和失败,并由独立检查核对输出。

    一个工程结果值得信任,不仅因为它在某次演示中工作,还因为别人知道它满足哪些条件、如何发现失败以及怎样恢复。下一篇会继续讨论:写出局部功能之后,系统责任还包括什么?

    来源、证据与阅读路线

    1. NASA Systems Engineering Handbook,第4章:利益相关方期望、需求与设计过程
    2. NASA Systems Engineering Handbook,第5章:产品实现、验证、确认与移交
    3. NASA Systems Engineering Handbook,Appendix C:How to Write a Good Requirement,需求清晰性与可核验性

    证据范围:本文以构造案例讨论需求、方案取舍与验收设计;尚未实现所述转换工具,也未执行性能测量或故障注入验证。

    版本1.2 · 2026-10-18:完成事实、条件与引用复核,逐篇明确证据方法;有实质缺口处补齐推导、反例或原题。原网址与首次发布日期保留。