作为一名工程师,我在科技行业和硅谷已经打拼十年。这十年,我一直在高速增长的创业公司工作,经历了与此相关的所有起起落落。从构建 nextgen 电子邮件客户端,到在全球范围内推广电动汽车,再到网上购物结账,我学到很多。

当回想过往,我觉得有些错误完全可以避免。在本文中,我将分享我在职业生涯中所学到的全栈工程师经验。从中,我总结了十大经验教训。我相信,这些经验教训值得起时间考验,并在未来几年里依然适用。

这份列表从前端开始,然后是后端 API 和数据库,最后是工程最佳实践 / 流程。

经验总结

  1. CSS Specificity

  2. 组件层次结构中的设计状态

  3. 后端编程中的面条式代码、千层饼式代码和馄饨式代码

  4. 生产中的 Postgres

  5. 欲速则不达

  6. 投资自动化

  7. 掌握你的工具

  8. 最小可行性产品(Minimum Viable Product,MVP)

  9. 研究支持开发

  10. 科学调试

1. CSS Specificity

错误:我的 CSS 不适用。我要用 !important

教训:应该为特殊情况保留使用 !important,因为它们破坏了整个 CSS 层次结构,并强制使用特定样式。所以,你有必要了解 CSS Specificity。

CSS specificity 是浏览器应用的一组规则,用来确定哪个 CSS 样式更 specific。你可以将其视为基于点的系统,它决定哪种 CSS 样式获得优先权,并最终应用于 DOM 元素。

如果你想知道,为什么你的 CSS 没有被应用,这与 CSS specificity 有关。在大型项目中,这是一个很常见的问题,在这类项目中,像 SCSS 这样的预处理程序与复杂的 CSS 层次结构一起使用。了解 CSS specificity 将有助于你保留使用 !important,仅在极罕见的覆盖情况下使用,例如,当你想要覆盖 CSS 库或让 iframe 覆盖主机站点样式时。

干了10年软件工程师,我学到10个教训
干了10年软件工程师,我学到10个教训

本质上,优先顺序是这样:ID selectors > Class selectors > Type selectors。!important 和内联 style 属性会覆盖所有的 CSS。

对于应用于元素的每个 CSS,你都可以轻松确定哪种样式将生效。例如,如果你加载上面的 HTML 代码:

干了10年软件工程师,我学到10个教训

在本例中,ID selector 优先级高于 type selector。如果冲突的 CSS 选择器具有相同的优先级,那么将选择 CSS 文件中的最后一个。

最后,Chrome DevTools 将会显示如上图所示的 specificity 顺序。如果你无法使用 CSS,请查看 Chrome 使用的 specificity 顺序,然后添加一个更具体的选择器(如 ID 选择器、类选择器、类型选择器),让你的 CSS 更特定,并指示浏览器选择它。

如果你不想这么做,请看看这个 specificity 计算器

2. 组件层次结构的设计状态

错误:我需要添加这个新状态。我只需要将它放到这个 reducer 中,但不确定为什么这个 reducer 还有这么多其他状态?

教训:管理不当的 redux 状态会在开发人员中造成混乱,并导致 bug。如果用 react 和 redux 来构建前端应用程序,那么你可以考虑使用这种可视化技术,从用户界面组件层次结构中构建状态和 reducer 层次结构。从零到统一的组件状态层次结构有三个步骤:

  • 在线框图(wireframe)将用户界面进行可视化。

  • 将状态层次结构进行可视化以反映用户界面。

  • 构建 reducer 层次结构以反映状态层次结构。

让我们来看一个例子。在这个例子中,我们构建一个博客网站,它有两个页面,一个页面用于博客列表,另一个页面用于个人博客:

第一步:在线框图中将用户界面进行可视化

干了10年软件工程师,我学到10个教训
干了10年软件工程师,我学到10个教训

主页和个人博客页面

第二步:将状态层次结构进行可视化以反映用户界面

相应的状态层次结构图如下所示:

干了10年软件工程师,我学到10个教训

状态层次结构

请注意,公共 Header 状态是如何被拉到根状态的。类似的,任何共享状态都可以在层次结构中“冒泡”,因此很明显,子组件共享该状态。

第三步:构建 reducer 层次结构以反映状态层次结构

干了10年软件工程师,我学到10个教训

reducer 层次结构

这是一个简单而强大的例子,展示了如何构造状态和 reducer 层次结构来匹配用户界面。这一过程可以轻松扩展到复杂的应用程序和大型团队。最后,你可以在这个结构上构建操作和表示层。

3. 后端编程中的面条式代码、千层饼式代码和馄饨式代码

错误:这个代码库是如何组织的?也许我可以在这里添加这个文件,就像所有其他仓库代码一样。

教训:我写过三种“意大利面条式代码”。说实话,我觉得在“馄饨式代码”里放上“迷你千层饼式代码”也许是个好办法。组织和培训所有开发人员以这种方式构建代码库,可以保持代码的可维护性、可测试性,最重要的是能保持敏捷性。你能在不影响其他功能的情况下,轻松修改特定的“馄饨式代码”(也就是功能)的实现细节。

“千层饼式代码”是:

  • 分层架构

  • 架构整洁

  • 外层是 I/O,内层是纯数据结构

  • 依赖关系向内注入

  • 内层不依赖于外层

  • 优先考虑组合,而非继承

“馄饨式代码”是:

  • Screaming architecture

  • 切片与分层

  • 文件夹和文件的空间位置

  • 可以是微服务形式

将它们放在一起,就能得到一个可扩展、可维护的代码库。如果你可以想出一种方法,通过特征名来组织文件夹,并且在每个特征中实现整洁结构,这将让你使用很长时间。

4. 生产中的 Postgres

错误:为什么这个查询速度这么慢?我认为是因为 Postgres 很慢的缘故。我需要切片,或者我认为这是 ORM,或者,我需要一个不同的数据库,Postgres 并不适合我。

教训:如果你在生产环境中运行 Postgres,那么你将会遇到查询缓慢、表锁、无限等待迁移和错误的问题。如果不是这样,那么对你有好处,你又是怎么做到的?这并不意味着 Postgres 不再是正确工具,而是意味着你需要揭开帷幕,看看下面发生了什么。

截至目前,我发现的最好工具是 pgbadger 。你可以用它解决几乎所有的 Postgres 问题。

这是一个 Perl 命令行工具,它将 Postgres(如果你使用 AWS 的话,就是 RDS)日志作为输入,并输出报告。该报告的好坏取决于你在 Postgres 上启用的日志。因此,在第一步时你可能需要启用这些日志:

复制代码

log_checkpoints = onlog_connections = onlog_disconnections = onlog_lock_waits = onlog_temp_files = 0log_autovacuum_min_duration = 0log_error_verbosity = defaultlog_min_duration_statement = 1s

此外,你可能还需要启用 pg_stat_statements 语句来实时分析查询,并启用 auto_explain 来自动解释日志中运行缓慢的查询。

运行报告:

复制代码

pgbadger --prefix '%m %u@%d %p %r %a : ' /pglog/postgresql.log

干了10年软件工程师,我学到10个教训

该报告将汇总数据,并提供有关 Postgres 所做工作的大量信息。你可以找到关于错误、最慢查询、等待最长查询、获取的锁类型、临时文件是否用于排序、检查点运行的频率、真空运行的频率以及其他类似信息。有了这些数据,你就可以识别和修复运行缓慢的查询,并通过调优提高 Postgres 的性能。

你可以持续运行此报告(CLI 支持增量模式),从而随时掌握新问题。

另外,如果你想理解解释输出,可以用这款工具

该工具将解释 JSON 和原始查询作为输入,并将在如下所示的可视化树形图中对解释输出进行解释:

干了10年软件工程师,我学到10个教训

正如你所见,节点将有最大、最慢、最贵等标签。这将帮助你根据 Postgres 的执行方式来优化查询。

最后,如果在 Postgres 中建立能力不可行,我建议你问问像 Percona 这样的数据库咨询公司。

5. 欲速则不达

错误:看起来不错嘛,让我们交付吧!

教训

干了10年软件工程师,我学到10个教训

在这个快速行动、打破常规的时代,这似乎不是最受欢迎的建议,但是“慢慢来”会有回报的。与其快速发布糟糕的代码,还不如有条不紊地来,发布几乎没有生产 bug 的代码。

优秀工程师会考虑到软件系统的所有问题

他们不仅关心代码覆盖率,还关心可能破坏相同代码路径的奇怪输入。通过分层架构,它们可以实现模拟层,并只测试所考虑的层。他们不仅实现单元测试,而且还实现了集成和功能测试。如果你的团队还有 QA 工程师,就与他们一起测试这些用例。

子曰:“无欲速,无见小利。欲速则不达,见小利则大事不成。”

6. 在自动化上投资

错误:我们手动部署到 staging 和沙箱,是临时的。生产也是手工部署,但每天执行一次。

教训:拥有一个 CI/CD 系统管理部署意味着更可预测的结果。软件以促销策略在 pipeline 中移动,而临时部署则被降级到特殊情况下。这可以确保你正交付软件的稳定性和可靠性,这是工程团队的主要责任。

投资:

  • 培训团队成员学习如何进行代码审查。你的团队成员可能有各种各样的技能,但不是每个人都知道如何进行更好的 code review。因此,你应在学习和教授 code review 的最佳实践上进行投资。

  • 使用像 peril 和 hound 这样的自动化 code review 系统。Peril 可以检查代码更改,并根据预先配置的设置,标记警告和构建失败。例如,如果数据库迁移文件缺少 statement_timeout 或包含不必要的 DEFAULT NULL,则 pull request 可能失败。你可以编写许多这样的检查和特定于团队的规则,并让 Peril 成为更改的“看门人”。HoundCI 可以做类似的事情,而且规则是完全可配置的。

干了10年软件工程师,我学到10个教训

  • 使用 CircleCI 之类的工具,通过自动推广策略设置 CI/CD pipeline。随着时间推移,优化构建和部署管道。

7. 掌握你的工具

错误:我需要找到这个实现接口,先搜索一下。我记得它以前就在这个文件夹里,现在却不见了?在那个文件夹里找找,我还是问问别人吧!

教训:不知道如何使用你的工具,这会让你效率变低。你能想象一个邋遢的裁缝使用缝纫机的样子吗?这不仅关系到代码结果,还关系到构建软件的效率。

干了10年软件工程师,我学到10个教训

了解你的工具,掌握捷径。代码编辑器可能是你要掌握的第一个工具。你应该知道如何设置选项卡的排序、打开最后的编辑文件、显示调用图等。如果你使用基于文本的编辑器,而不是图形用户界面,这也可行。类似 Vim 这样的编辑器,有很多实用技巧。

请注意手动执行的常见操作,并学习如何通过快捷键来执行这些操作。要做到这点,一个简单的方法是先记住 5 条捷径,并熟练掌握它们,然后再记住 5 个捷径。

全栈工程师每天接触的其他常用工具有终端、docker、tableplus/pgadmin/ 一些其他数据库客户端用户界面、Chrome 开发工具等。

8. 最小可行性产品(MVP)

错误:我觉得这个功能会很有用。我要使用分布式容错复制高可用数据存储。我还要构建一个基于插件的架构,使这个软件具有超强的可扩展性。

教训:在构建某个东西前,请确保它是你要构建的正确的东西。这就是 MVP 的用武之地。

理想的最小可行性产品(Minimum Viable Product,MVP)应该尽可能少地触及所有层面,而不仅仅是一个层。这是降低风险的一种做法。最好是最低限度地构建所有层,而不是完善单个层。最小可行性产品并不意味着技术债、糟糕的代码或缺乏测试。它不是抛弃型代码(throw-away code)。

如果最小可行性产品花的时间太长(在某种程度上),那么它就有可能是错误的方案,并且可能有更简单的解决方案。

奥卡姆剃刀:在其他一切同等的情况下,较简单的解释普遍比较复杂的好。

9. 研究支持开发

错误:我(工程师)认为这是我们应该构建的。

教训:在开发前,应该先进行大量的研究以佐证。与其跟随你的直觉,不如进行一项用户研究。要了解用户需求,可以亲自或者通过视频采访,进行调查、查看日志等。这将帮助你更好地了解用户。然后,你可以提出一个假设并进行实验。当形成一个假设时,请使用反演来驳斥自己的主张。

在一个 A/B 测试框架上投资,可以让你进行实验。

时间宝贵,要明智使用。最聪明的工程师会尝试优化一些不应该存在的东西。尽早提出正确问题非常重要。

10. 科学的调试

错误:有一个 bug。我想是因为代码改变所致。让我看看这个文件。没准也许是内存问题所致。或者这两个原因都有可能。

教训:作为一名工程师,无论是作为事件一部分,还是在本地环境中,你都将调试软件中的问题。如果不是通过结构化推理来完成的话,调试可能会非常痛苦、缓慢。

我们如何系统找出程序失败的原因?如果没有“直觉”、“敏锐思维”等模糊的概念,我们又该怎样才能做到这一点?我们想要找到一种查找失败原因的方法——这种方法:

  • 不需要先验知识

  • 是有系统的

  • 我们可以确定找到根本原因,并随意复制

将科学方法应用于调试问题,是发展失败理论的公正方法。科学调试的步骤如下:

  1. 重现错误(通常是一些时间、数据、用户、操作系统、调试器的组合)

  2. 观察事实(彻底读取日志、错误跟踪等)

  3. 在日志中明确地陈述假设,而不是在心里做假设

  4. 如果你发现程序中的某部分存在错误,使用结构化方法来缩小错误范围,如二分搜索

  5. 测试假设:使用日志、断点、断言

  6. 如果通过验证,应用修复并确保没有新错误

  7. 如果无效,请重复步骤 3 到步骤 6

对简单的调试情况来说,这可能看起来有点过头。但是,对于涉及多个团队的复杂分布式系统而言,一个系统科学的调试过程为消除模糊性提供必要的结构。

额外福利

1. 分享知识,服务他人

优秀的行为是帮助他人成长。当你需要用别人能理解的方式来解释某事时,你对事情的理解会更清晰。

每天在 Slack 上分享有思想的链接,进行演示、称赞他人的积极行为,挑战不明确的决定,并在你希望与某人或某项决定有着不同的方向时,给予建设性的反馈。你可以使用“感谢 ABC……希望 XYZ”的句式表达你的反馈。

通过这样做,你可以为自己打造个人品牌,从而获得职业资本。研究表明,那些拥有强大个人品牌、网络形象和帮助他人的记录的人,会取得成功,更重要的是,还会拥有令人满意的职业生涯。

2. 塑造自己的世界

你无需接受这一现实世界。你可以通过坐在驾驶座上,塑造你所感知的世界。

这可能意味着在设计讨论和 code reviews 期间发表意见,或者修复关键的不稳定的测试(flaky test)。很多人会告诉你要多发言,提高知名度,以便在公司内部发展,但他们却从来不告诉你怎样才能做到这一点。要做到这一点,最好的方法是拥有坚定的观点和信心,将人们拉向你的方向。不要畏惧组建小型团队来构建 / 改进事物。不要屈服于你的恐惧。要大声说出来,只要不是无礼的,你都可以说出来。

负面情绪是改变的巨大动力。如果有问题让你感到困扰,你要扪心自问,并想出该如何进行改变。如果你将每一天都当作成长的途径,那么生活就会成为一种锻炼。

3. 结识朋友

如果你像我一样,想弄清楚什么对你真正重要,那就去结识许多朋友吧,尤其是那些让你感兴趣的人。这可能意味着去参加会议、参加在线社区或者在黑客松活动和项目上进行合作。这种接触会帮你弄清楚你想要做什么。这样,你就可以对那些无关紧要的事说“No”,并抓住对你来说重要的那些机会。

许多成功人士感到幸运,宣称只是因为他们在一个正确的时间和正确的地点做了正确的事情而已,从而知道自己想从什么开始。这让他们能随机应变,抓住机会,减少遗憾。遇到聪明人时,不要轻易拒绝。

通过这样做,我发现我更喜欢的是广度而不是深度,更看重创造力和自由,喜欢多样化和非正式的关系。我不适合从事结构化的重复性工作、例行公事、稳定和安全的事情。

如果你知道你想要什么,世界会给你所需的信息。

全栈编程充满乐趣。这是一个不断发展的领域,有一片学习冲浪的海洋。不要把自己或错误看得太重。分享它们,不断成长。

作者简介:

Pranay Suresh,工程师,供职于 Bolt 公司。曾在 Tesla 供职,硅谷创业者。他是佐治亚理工学院(Georgia Tech)计算机科学硕士,同时也是博主、演讲者和导师。个人主页

英文原文:

10 FullStack Engineering Mistakes to Avoid