如果遵循这些建议会让你的代码变得非常冗长和/或复杂(比如防御式代码),你可以需要对其进行重新设计
"scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", "lint:errors": "eslint . --quiet", "lint:typescript": "tsc --noEmit --skipLibCheck", "lint:jsdoc-typing": "tsc --noEmit --allowJs `find ./ -name '.js' -name '.d.ts'`" },
复制代码借助静态代码分析器和 npm 脚本,能够让开发人员轻松快速地探测有问题的代码后续该怎么办?安装和配置静态代码分析工具是一个良好的开端,但这还不够要想取得持续的成功,要确保开发团队做到如下几点:充分认识到部署不含编程错误的代码的重要性,并相信静态代码分析可以帮助他们实现这一点;充分理解 TypeScript 的运行原理(参见TypeScript: Handbook)定期修复警告和类型错误,起码要比引入它们的频率更高;保持这些措施,永不间断如下几种策略可能会提供帮助:奖励提高代码质量的代码贡献行为,从而激励开发人员其中,有种方法是使用可插入持续集成流水线的工具来跟踪开发人员推送的每个变更的代码质量变化,例如可以使用 SonarCloud 和/或 Codacy让一名开发人员负责确保代码质量永不下降让另一名开发人员负责定期更新依赖,从而能够让团队能够从它们的逻辑和安全修复中受益为何要把每个角色都交给一个专门的人?当某项职责没有人负责时,集体责任往往会被其他“优先事项”所取代(比如,本周多交付一个特性,但是代价是忽略一个警告)定期轮换角色,确保每个人都能参与其中并保持积极性使用(恰当类型的)测试覆盖关键的业务逻辑现在,我们有了一支致力于保持代码库整洁的团队,我们相信用户很少会遇到编程错误但是,业务逻辑中的错误该怎么办呢?例如,如果一个新添加的功能破坏了另一个功能该怎么办?如果开发人员从一开始就误解了该功能的预期行为,又该怎么办?如果这样的错误最终导致了严重的收入损失又该如何处理?与编程错误类似,业务逻辑问题可能会在生产环境由用户发现,但我们更希望尽早发现它们因此,定期测试软件非常重要,这个过程可以使用自动化和/或手动测试从业务角度看,测试有两个作用:符合功能性需求:每个特性的实现都能满足开发时的需求检测回归:在对代码进行任何修改后,所有现有的特性都能按照预期运行确保功能性测试(也称为“验收测试”)涵盖大多数关键业务特性,单元测试或集成测试涵盖大多数关键技术组件此外,确保持续集成在任何测试失败时都能向开发人员提供可执行的反馈对于有些开发人员来说,将测试工作委托给其他人(如产品负责人或 QA 团队)是很有诱惑力的做法在每个新特性完成后,进行一次这样的委托测试,以确保特性实现符合功能性需求,并进行协作迭代,这样做可能是合理的但是,委托他人进行回归检测并不是一个好主意,原因包括:它增加了合并代码和部署代码之间的延迟它增加了发现回归问题和修正它们之间的延迟随着功能性范围的不断扩大,检测回归所需的时间也会随之增长如果负责这些测试的人没有将其自动化,他们最终可能会跳过越来越多的测试因此,一段时间之后,出现回归测试未发现问题的风险就会越来越高回归测试是一项痛苦且可能代价高昂的负担,尤其是需要不同角色(如产品负责人和开发人员)必须协作的情况下从长远来看,回归测试自动化意味着可以节省大量的时间,而且开发人员具有编写自动化测试的技能,所以,开发人员首先要承担起检测回归的责任,而不必让其他角色参与进来,这符合他们的利益如果要涵盖的功能范围很大该怎么办?从最关键的业务特性开始要找出这些特性,你可以问自己:“就收益和/或减少成本而言,在生产环境中可能发生的最糟糕的事情是什么?”例如,电子商务网站的回答可能是如下的特性:“信用卡购物”特性每分钟可以带来大约 1000 美元的收入如果销售人员必须要求首席技术官手动将产品添加到数据库中,则“将产品添加到目录中”特性每小时的成本约为 500 美元如果客户支持团队需要手动处理订单,那么“打印条形码以退回订单”将使我们每天损失 500 美元基于这些业务关键的用例,从它们开始编写端到端的自动化测试肯定就是非常有意义的何时运行测试?在每次代码更新或添加到代码库之时,在将其部署到生产环境之前借助git hook,在每次提交时运行测试可能就足够了,因为它能可靠地运行,而且其持续时间不会导致开发人员编写更少的测试不管是否使用git hook,都要确保每次推送可用于生产环境的代码时,测试能在某处运行(例如,最好是在持续集成环境中)在持续集成环境中,每次提交都会运行代码检查和自动化测试我们应该编写什么样的测试?需要优化的变量包括:测试所覆盖的功能性和技术性范围的大小从测试中获得反馈的时间修复失败测试所报告的问题所需的时间因为误报而损失的时间(即由于随机原因导致失败的测试)如果你的团队在编写自动化测试和/或可测试代码方面经验不足,那么可以从一些端到端测试开始然后,逐步增加对范围更小的代码单元的测试这样做可以激励开发人员编写易于测试的代码例如,通过隔离责任、减少耦合和/或将业务逻辑写成纯函数遵循依赖注入架构是实现这一目标的好方法(参见六边形架构或简洁架构)我们是否应该 Mock 第三方 API?自动化测试(如本文所述)的目的是探测团队的功能性范围内的回归,而不是第三方的功能基于这一点,在测试中 Mock 第三方是合理的也就是说:Mock 应始终与当前 API 的行为相匹配这意味着开发人员需要持续关注 API 的变化,并相应的更新它们的 Mock当实际 API 的行为与预期不符时,你可能依然希望得到警告探测自己的代码中的问题和第三方 API 中的问题并不遵循相同的生命周期:每次代码进行变更时,所涉及的范围都应该进行测试仅在第三方的代码发生变更的时候,才应该对其进行测试(也就是说,每次提交代码变更都测试第三方依赖是没有什么意义的)你需要持续监控第三方提供商是否能够正常运行并达到预期效果但是,第三方错误不一定能够在发生之时就探测到,因此最好是定期监控,而不是在开发人员每次推送代码变更的时候进行监控所以,需要搭建两个专门的流水线:你自己的 CI 流水线会在你的代码发生变更的时候测试自己的范围另外一个 CI 流水线定期检查第三方所涉及的范围是否按照预期运行为了编写长期最有用、最健壮的测试,我建议遵循F.I.R.S.T.原则确保开发人员不会滥用mock细致保护代码库中新的/现代化的部分假设你的代码库已经或者将要开发数年的时间,那么随着时间的推移,它很可能会在代码风格和质量方面失去内聚力更糟糕的是,由于技术债务、缺乏测试或意外复杂性的积累,某些组成部分的维护可能会变得很复杂在这种情况下,要像上文所建议的那样,在整个代码库中对代码实现一致的内聚预期可能会变得很复杂不过,这也没有关系你不希望看到的是期望值降低到一个最低的平均水准这样的话,你可以把代码划分为不同的范围,并为每个范围设置不同的期望水平例如,考虑一个即将为电子商务网站实现新特性的团队他们希望这个新特性能够比代码库中的其他特性更健壮、更易于维护为了实现这一点,他们在配置静态代码分析工具(如 ESLint 和 TypeScript)时采用比代码库的其他部分更严格的规则,并针对专门为该特性而创建的目录使用覆盖的方式启用更多的规则通过这种方式,团队可以提高新代码的质量,而不必急于对代码库中“遗留”的部分进行现代化处理"rules": { "prettier/prettier": "error", "deprecation/deprecation": "warn" }, "overrides": [ { // Tolerate warnings on non critical issues from legacy JavaScript files "files": [".js"], "rules": { "prefer-const": "warn", "no-inner-declarations": ["warn", "functions"], "@typescript-eslint/ban-ts-comment": "warn", "@typescript-eslint/no-var-requires": "off" } }, { // Enforce stricter rules on domain / business logic "files": ["app/domain//.js", "app/domain//.ts"], "extends": ["async", "async/node", "async/typescript"], "rules": { "prefer-const": "error", "no-async-promise-executor": "error", "no-await-in-loop": "error", "no-promise-executor-return": "error", "max-nested-callbacks": "error", "no-return-await": "error", "prefer-promise-reject-errors": "error", "node/handle-callback-err": "error", "node/no-callback-literal": "error", "node/no-sync": "error", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/promise-function-async": "error" } } ]
复制代码通过配置覆盖,我们可以为不同的部分设置不同的 ESLint 规则与之类似,如果要对整个代码库进行现代化改造,也要循序渐进你可以创建一个具有更严格规则的专用目录,并逐渐将遗留代码迁移至该目录,同时修复代码的警告和类型错误从何处开始?有种方式是逐步将功能范围中陈旧的部分迁移到更好的设计中例如,选择一个难以编写自动化测试的特性,并将它的实现迁移到六边形架构中,将业务/领域逻辑根据输入命令(即“API”)和副作用(即“SPI”)分离开来通过编写自动化测试来指导迁移,并将新的实现放在具有更严格静态代码分析规则的专用目录中import { makeFeatures } = from './domain/features';import { userCollection } from './infrastructure/mongodb/UserCollection';import { ImageStorage } from './infrastructure/ImageStorage.js';/ @type {import('./domain/api/Features').Features} Features/const features = makeFeatures({ userRepository: userCollection, imageRepository: new ImageStorage(),});routes.post('/avatar', (request, response) => { features .setAvatar(request.session.userId, request.files[0]) .then( () => response.json({ ok: true }, (error) => response.json({ ok: false }) );});
复制代码setAvatar特性经过了重新设计,由于采用了依赖反转,使其易于单独进行测试下面是我们迁移另一项特性的过程,即播放列表删除如果你决定遵循这一路径,如下是一些建议:如果你的团队没有重新设计遗留特性的经验,那么就从简单的小特性开始否则的话,请选择一个未来几周或几个月内要实现的特性最依赖的那个特性在编码之前,明确范围、业务事件和路径例如,与你想重新设计的领域(或限界上下文)所涉及的专家一起组织一次事件风暴可视化要迁移范围的当前架构,例如使用像ARKit、Dependency-Cruiser或类似的依赖分析工具,并写明不想在目标架构中重复出现的问题,以免重蹈覆辙如果有疑问的话,请使用软件设计工具(如时序图、状态机图、ADR)协作完成恰当的设计在迁移完每个限界上下文之后,你将会得到一个代码库,在代码库中 100%的代码都应按照更严格的规则进行检查每日部署,但同样的错误不要犯两次尽管使用了静态分析工具来检测缺陷,使用了自动化测试来探测回归,但用户还是会在生产环境中发现问题这是无法避免的但是,有一种方法可以降低出现此类问题的概率,并缩短团队修复问题的时间:每日部署(前提是你确信失败的风险很低)同样的错误不要犯两次为何要每日部署?简约版答案:因为DORA研究项目发现,大多数执行团队每天都在进行部署,或者每天部署多次详尽版答案:因为这能够让开发人员更快地找到在生产环境中出现新缺陷的根本原因也就是说,部署越频繁,最新部署和上次部署之间的提交次数就越少基于相同的原因,如果最新版本不能按照预期运行,回滚到上一个版本的成本会更低(就回滚代码提交的次数而言)因为这能鼓励团队将工作分成更小、更安全的增量DORA 认为,这也是表现最好的团队所遵循的做法如何确保相同的错误不犯两次?在生产环境中出现意料之外的行为是可以的在有些情况下,这甚至是一件好事当意料之外的行为给企业和/或开发团队带来巨大损失时(例如,网站中断,导致几个小时无法使用),开发人员应该采取措施防止类似的事件再次发生如何探测生产环境中的问题?有多种方式可以探测生产环境中的问题:理想情况:开发人员发现问题并立即修复常规情况:员工发现问题并向开发团队报告更糟糕的情况:用户向开发团队报告问题最糟糕的情况:用户发现了问题,但并没有报告无论是哪种情况,开发人员都需要以下信息:问题是什么、问题的具体表现(如错误信息)、如何重现问题(如环境+过程),以及用户的初衷和期望是什么但是,如何在最糟糕的情况下获得这些数据呢?这就是错误监控工具(如Sentry)的用武之地了通过将它们注入到生产环境中运行的产品中,它们就能像探针一样检测运行时错误,并将它们汇总到已知错误的列表中,直到每个错误都被开发人员修复为止此外,它们还会获取有关错误上下文的数据(如用户代理、所使用软件的版本、操作系统、确切的时间戳等),以帮助开发人员重现错误但令人遗憾的是,与静态代码分析器类似,这些工具并不能解决问题因此,与警告和类型错误一样,要确保尽快处理每个错误团队让错误累积得越多,使用这些工具的动力和效率就会越低此外,在使用这类监控工具时,请确保个人和/或机密数据不会从系统中泄露出去从战术上讲,有许多方法可供选择你可以让一名开发人员负责修复生产环境的错误,并将其作为最优先的事项这个角色可以定期轮换(比如每天),这样可以激励每个人都编写更健壮的代码或者,也可以在每天的会议上将新错误单独分派给志愿开发人员如何降低复发风险?不必慌张当生产环境中发生事故时,都要遵守如下程序:保留事故发生前、发生时和发生后的痕迹,以帮助你进行事后分析(注意:在事故发生前做好充分的监控和日志收集工作)在内部和外部就事故进行沟通稳定生产环境,例如,回滚到之前能正常运行的版本编写并部署修正版本,以修复问题查找并解决导致问题的根本原因,并采取预防措施避免重蹈覆辙的关键在于上述程序的最后一步这也是经常被忽视的过程大多数情况下,是因为没人觉得自己有责任这样做很多时候,是因为产品负责人(或产品团队)向开发人员施压,要求他们优先完成开发计划中的特性,而不是保护现有代码和/或调整开发流程有时,开发人员自己也会决定开发更多的特性,而不是避免再次犯错调查事故根本原因时的注意事项如何查找事故的根本原因?在这个方面,“5 个为什么(5 WHY)”技巧是很有用的例如:生产系统为什么会崩溃?——因为一个未登录的用户访问了页面 B用户为什么能够访问页面 B?——因为主页上有一个链接用户在访问页面 B 的时候为什么没有看到登录页面?——因为在页面渲染时,后端还不知道登录状态为什么页面渲染时还不知道登录状态?——因为我们的会话管理后台很慢,等待这个状态会大大降低我们的网络性能指标为什么会话管理后端很慢?——因为它使用的是未经优化的遗留数据库在本例中,根本原因是整个网站都依赖于遗留的会话管理后端,这使得导航难以预测,有时还会导致生产环境崩溃因此,除非团队修复传统的会话管理后端,否则类似的崩溃很可能很快就会在生产环境中再次发生团队现在应该修复遗留的会话管理后端吗?也许不用但是他们应该努力制定一个能够实现该目标的补救计划在实践中,如何实现低故障率的日常部署呢?让一位开发人员负责确保尽快发现生产中的意外行为(如运行时错误、缺陷、事故……),尽快修复,并采取措施防止今后再次发生各类问题通过这种方式,开发人员能够感受到有能力在良好的条件下开展工作例如,在生产过程中设置恰当的监控和日志,确保撰写有用的事后报告,并采取预防措施当信心达到良好水平时,逐步增加部署频率以正确的激励机制调整产品开发团队此时,开发人员就具备了编写高质量软件,并尽快发现缺陷的能力这些缺陷最好是在设计或实现时发现,而不是在生产环境中他们能够快速发现并修正生产环境的错误,不会重复犯同样的错误他们对自己的代码和开发流程充满信心,因此每天都能在生产中实现改善而且,他们在对软件功能化范围进行预期改善的同时,也会逐步改善代码库中最古老部分的设计和质量,使其保持健康、稳健并易于长期维护但是,令人遗憾的是,这种平衡很快就可能被瓦解举例来说:如果开发人员失去了长期保持高设计标准和/或代码质量的动力如果部分开发人员不遵循团队的质量准则,造成系统性返工、挫折和延误如果开发人员误解了功能性需求,而急于修复无法达到预期效果的特性,从而牺牲了长期的技术责任如果有人(如经理、产品负责人或其他人)向开发人员施压,要求他们每周发布更多的特性,或在紧急的期限内完成任务如果激励和/或奖励开发人员的绩效指标与其代码库的长期质量和健壮性不一致例如,根据每周交付的特性数量确定晋升奖金防止或解决这类情况可能会非常困难,因为这需要良好的领导力和/或软技能一个常见的错误是培养某种思维定式,即开发人员应该主要致力于实现优先的、计划好的和设计好的特性这样做是有问题的,因为:它要求开发人员处于这样一种状态,即对软件做的每一项变更都要有精确和明确的规范这可能会导致开发人员无法与负责制定这些规范的人员进行健康的双向合作对于那些喜欢整天独自工作的开发人员来说更是如此它让开发人员处于这样一种境地,即难以衡量那些与功能性路线图没有直接贡献的开发活动,如更新依赖、提高代码质量、培训更好的设计和编码技术这很容易让人倾向于根据指标(如用户故事的开发速度)来跟踪开发人员的绩效(或“生产力”),而忽略了对可持续开发实践的投资,即代码质量、阻碍回归、错误管理等下面是一些关于如何避免上述陷阱的建议:在详细阐述业务问题的解决方案时,至少让一名开发人员参与设计过程这将提高开发人员的责任心,使他们能够为一个充分理解的问题实现一个好的解决方案有时,由于开发人员了解当前的建模和实现方式,他们会提出替代解决方案,从而在满足需求的同时节省大量的开发时间确保产品和技术代表能够公开、友好地协商功能性和技术性项目的优先级和规划例如,如果开发人员需要重新设计代码库的某个部分,那么他们就应该说服其他人相信这一点的重要性,解释这将为下一个特性的开发带来哪些具体的改善,以及延迟该项目的风险和成本是什么同样的建议也适用于产品经理如何对即将开发的功能改善进行优先排序和规划:通过解释来说服开发团队并让他们参与进来这样做可以增强参与设计和实现特性的所有员工的信任、协作和参与度在管理方面,确保开发人员不会得到这样的激励,即“每周尽可能多地发布特性”找到使每个开发人员的职业目标与团队的短期和长期期望相匹配的发展轨道这样做的目的是防止出现开发人员理直气壮地只从事短期改善相关工作的情况最后,确保为开发人员提供资源和指导,以不断提高他们的软硬技能为他们提供培训和/或指导资源鼓励他们通过结对和/或群体编程的方式共同完成任务鼓励他们与其他/非开发人员角色进行良好的协作,包括领域专家、产品负责人、产品设计师、客户支持团队、终端用户等结论JavaScript 语言及其不断变化的软件包和实践组成的生态系统会使代码库迅速变得难以维护正如我们在本文所讨论的那样,无需从头重写所有的内容,也无需暂停新特性的开发,就可以避免由此造成的开发速度和/或代码质量的下降关于如何在 TypeScript 和 JavaScript 项目中应用这些推荐做法的更多实用建议,我建议你参考Yoni Goldberg的最佳实践列表它们是为 Node.js(后端)项目编写的,但其中很多也适用于前端代码库原文链接:前端老手10年心得,JavaScript/TypeScript项目保养实用指南_架构/框架_InfoQ精选文章
(图片来源网络,侵删)
0 评论