作者丨Neil Kakkar
译者丨核子可乐
一年之前,我开端在彭博担任全职作业。从那时起,我就在构思这篇文章。我幻想自己可以在时机成熟时,把自己的主意都倾吐于纸端。但刚刚曩昔一个月,我就意识到这并非易事:跟着作业的推动,我遗忘了许多自己刚刚学到的东西。这些东西快速内化,使我的大脑开端诈骗自己,令我误以为自己早就把握了这些明晰记住的常识,或许是承认自己从未听说过那些实践上是被忘记了的内容。正由于如此,我才开端保存自己的日志。每逢遇到风趣的情况,我都会把它记载下来。感谢坐在我身边的高档软件工程师们,我可以仔细调查他们在做什么、与我的做法又有何差异。咱们会常常结对编程,这可以大大下降作业的难度。其他,在咱们的团队文明傍边,“窥视”其别人的编码进程并不是什么不光彩的作业。每逢我感觉风趣的作业要发作时,总坐很快转过身去检查。这种敏锐,让我总能快速澄清作业的来龙去脉。下面来看看坐在一位高档软件工程师身旁一年,我都学到了哪些重要阅历。
编写代码
怎么命名
我在作业中触摸的榜首项使命是开发一款 React UI。其时咱们具有一个主组件,用于包容其它悉数组件。我喜爱在代码傍边加点诙谐元素,所以我把它命名为 GodComponent。但在代码检查时,我才意识到为什么命名作业如此重要、也如此困难。
计算机科学范畴有两大难题:缓存失效、命名以及缓冲溢出过错。—— Leon Bambrick
我命名的每一段代码都包括躲藏的含义。GodComponent?这个组件的含义,便是我会把悉数不知道该放在哪的组件都放在这儿。它包括悉数。假如我把它命名为 LayoutComponent,后续我才会意识到它的效果便是布局分配,其间不包括任何情况。
我发现的另一项心得在于:假如其体积过于巨大,就像是这儿说到的包括许多业务逻辑的 LayoutComponent,那么我就会意识到是时分进行重构了,由于经过称号就能看出业务逻辑并不归于这儿。但运用 GodComponent 这个称号,咱们无法判别业务逻辑呈现在这儿是否正常。
怎么命名集群?最好是在运转了服务之后再对集群进行命名,然后依据运转内容的改动从头调整称号。终究,咱们用自己的团队称号完结了集群命名。
函数命名的情况也是相同。doEverything() 这个姓名就不怎么样,其会带来严峻的效果。假如这项函数可以完结悉数操作,那么咱们将很难测验函数傍边的某些特定部分。并且不论这个函数有多大,咱们都会觉得很正常,终究它的姓名可是叫“everything”。所以,最好的办法当然是替换称号,进行重构。
可是,咱们在命名中也要考虑到另一类问题。假如称号的含义过分详细并疏忽了某些细微差别,该怎么办?例如,在 SQLAlchemy 傍边调用 session.close() 时,封闭会话不会封闭根底数据库衔接。(我本应该跳出手册约束,对这项 bug 进行处理,详细情况将在调试部分进一步阐明。)
在这种情况下,咱们可以考虑 x, y, z 这样的称号,而非 count(), close(), insertIntoDB(),然后避免为其分配隐含的含义。过分详细,会迫使咱们不得不在后续保护时吃力检查这些函数到底是用来干嘛的。
终究,其时的我历来没想到命名会成为值得独自一提的重要作业。
留传代码与下一位开发者
咱们有没有面临一段代码时,感觉摸不着头脑?他们为什么要这么写?这彻底说不通啊。
我就“有幸”接手过留传代码库。其间就存在相似于“跟穆罕默德承认过情况之后,撤销注释”这类阐明。这话是谁说的?穆罕默德又是哪位?
在这方面,咱们无妨做个人物转化——考虑下一位接手我所编写代码的开发者。他们相同会发现我的代码十分古怪。同行评定可以很好地处理这个问题。这不由让我想到上下文准则,即:了解团队开展作业时的实践境况。
假如我跑去忙其他事,稍后又回来,我或许也无法从头树立这种上下文。我坐说,“其时我是怎么想的?这底子没道理……哦等等,我原来是这么干的。”
正是为了完结这种提示效果,文档与代码注释才会如此重要。
文档与代码注释
文档与代码注释的含义,在于坚持上下文并共享常识。
正如 Li 在怎么构建杰出软件中所言,“软件的首要价值并不在于生成的代码,而在于生成代码的进程中开发者所堆集下来的常识。”
“软件的首要价值并不在于生成的代码,而在于生成代码的进程中开发者所堆集下来的常识。” - Li
咱们其时有一套面向 API 端点的随机客户端,好像历来就没人用过。那么要不要把它删去去?终究这也归于技能债款。
但假如我告知咱们,每年在特定的国家 / 区域,都会有 10 名记者将新闻发送到该端点,又该怎么办?咱们是怎么测验的?假如没有文档(也的确没有),咱们找不到答案。因而,咱们删去了该端点,并在对应时刻点上发现了问题——这 10 名记者无法发送 10 份重要的报导,由于该端点现已不复存在。
了解产品的成员现已离开了团队,现在只能靠代码傍边的注释来解说该端点的效果。
从这件事上,我意识到文档是每个团队都在尽力处理、但却难以见效的问题。除了代码文档之外,与代码相关的流程也有相似的情况。
时至今日,咱们也没有找到完美的处理计划。
原子提交
假如有必要要回滚(并且回滚需求迟早会呈现,咱们将在测验部分详细评论),此次提交仍是否有含义?
在删去废物代码时要充满信心
删去废物或许过期的代码总是让我感觉很不舒畅。我总觉得以往的作业效果有种神圣不可侵犯的含义。我那时分以为,“在他们写与这些代码时,必定是有所考量的。”这是一种传统的了解办法,并且与榜首性准则有所抵触。出于相似的理由,我在每年进行代码检查与整理时也是困难重重。这样的糟糕习气,让我吃了不少苦头。
我从前测验调整代码问题,也有些老成员习气于绕过这些代码。但删去,删去听起来更严峻正派。一个永久用不上的 if 句子、一个永久用不上的函数,会在我的一声令下彻底消失,这样欠好。因而,我更多是把自己的函数掩盖在上面。但这并没有削减技能债款,仅仅添加了代码的复杂性与误导性。如此一来,后继者将更难把这些片段以有含义的办法凑集起来。
我现在采纳的办法是:总会存在咱们无法了解的代码,也总会存在咱们永久不会运用的代码。删去这些永久不会运用的代码,但对无法了解的代码坚持慎重的情绪。
代码检查
代码检查是学习中的重要组成部分。检查的进程,便是从编写代码、到了解怎么更好地编写代码的反响循环。咱们自己的编码思路,跟其别人的编码思路有何不同?我在每一次代码检查时都会问自己:“他们为什么要这样做?”假如真实找不到合理的答案,我就会跟他们当面聊聊。在榜首个月的过渡期完毕之后,我开端张狂地从搭档的代码傍边查找过错(当然,他们也不会放过我)。真的很张狂,这也让评定作业变成一项风趣的调剂——或许说像是一种游戏,可以改进咱们编码水平的小游戏。
我的心得:在了解代码效果之前,不要轻下断语。
测验
我特别喜爱测验这项作业,事实上假如不加测验,我底子就不乐意直接在代码库中编写代码。
假如您的整个运用程序只需求履行一项使命(我在校园里的实验性项目便是这样),那么手动测验即可处理问题,我曾经也一向习气于这种办法。可是,当运用程序傍边包括上百种功用,情况又会怎么?我不想拿出许多时刻挨个测验,并且我也知道自己必定会遗忘某些需求测验的部分。这必定会是一场噩梦。
这时分,咱们就该请出测验自动化计划了。
在我看来,测验跟记载文档差不多。测验的进程,便是记载我关于代码的假定是否正确的进程。测验会告知我,我自己(或许是最初写下代码的开发)其时期望代码怎么运转,以及以为哪里有或许出问题。
因而,现在再编写测验时,我会紧记以下两点:
演示怎么运用我正在测验的类 / 函数 / 体系。
展现我以为或许出问题的部分。
榜首条信任许多朋友都能了解,终究在大多数情况下,咱们需求测验的其实是行为,而非完结。但我个人总会疏忽第 2 条,即 bug 或许呈现在哪里。
因而,每逢我发现 bug 时,我都会保证代码修正程序在相应的测验(也便是回归测验)傍边记载下其它有或许引发过错的办法。
当然,编写这类测验自身并不能供给代码质量,只要真实编写代码才会真实影响质量。不过我从阅览测验效果傍边取得的见地,的确可以协助自己编写出更好的代码。
这便是测验的微观含义。
除此之外,测验还肩负着另一项重要使命:承认布置环境。
咱们或许具有完美的单元测验,但假如没有进行体系测验,就有或许发作以下情况:
锁到底是好的,仍是坏的?
关于经过杰出测验的代码也是如此:假如您的机器上没有其需求的库,代码就会溃散。
您开发地点的机器环境。(「悉数都能在我的机器上正常运转!」)
您测验地点的机器环境。(或许便是您开发所运用的那台机器。)
终究,您布置地点的机器环境。(请必定换一台其他机器。)
假如测验与布置机器间的环境不匹配,那一般都会出点问题。而这,正是布置环境的含义地点。咱们在自己的机器上运用 docker 构建本地开发环境。
在这套开发环境傍边装置有一组库(及开发工具),咱们则以此为根底装置现已编写完结的代码。悉数与其它依靠体系相关的测验,都在这儿完结。
然后是 beta 测验 / 分段环境,其与出产环境彻底共同。
终究是出产环境,也便是担任运转代码并为实践客户供给服务的机器。
咱们的基本思路是尽力捕捉那些不会在单元与体系测验中呈现的过错。例如,恳求与呼应体系之间的 API 不匹配问题。
我猜个人项目或许小型企业的情况或许有所不同,终究并不是每个人都有资源来设置自己的一套根底设施。可是,假如咱们乐意运用 AWS 以及 Azure 等云服务,这儿说到的办法依然合适各位。咱们可以为开发以及出产环境设置独自的集群。AWS ECS 运用 docker 镜像进行布置,因而各环境之间相对共同。比较扎手的部分,便是假如与其它 AWS 服务顺畅整合。例如,咱们是否从正确的环境中调用了正确的端点?
咱们乃至可以更进一步:为其它 AWS 服务下载备用容器镜像,并运用 docker-compose 指令设置完好的本地环境。这样可以加快反响循环。
如此一来,当我的顺便项目发动并开端运转之后,我就能堆集到更多阅历心得。
消除危险
所谓消除危险,便是在布置代码的进程中尽或许下降危险水平的一种艺术。
那么,咱们可以采纳哪些办法来消除灾难性效果?
假如咱们期望推出的一项突破性的改动,那么一旦呈现问题,假如保证业务尽或许不受严峻影响?
“咱们不需求对悉数的新改动进行全体系布置!”哦,是吗……抱愧,我没想到。
设 计
许多朋友或许会问,我为什么要把规划放在编写代码与完结测验之后?好吧,规划在实践流程中或许比较靠前,但假如没有在当时环境中进行编码与测验,我个人很难规划出一套可以与特定环境完美适配的体系。在规划体系时,咱们需求考虑许多问题,包括:
资源运用量是多少?
存在多少用户?估计用户会以怎样的速度添加?(这将直接决议未来存在多少数据库行)
未来或许呈现的圈套是什么?
我需求把这些转化成一份名为“要求汇总”的清单。现在我还没有堆集到充沛的相关阅历,依据计划,下一年我的作业内容便是着力处理这方面问题。
这个进程有点违反灵敏准则——在开端施行之前,咱们可以做出多少规划判别?这是个权衡问题,咱们需求挑选在怎样的时刻点上做什么。咱们什么时分该深化分析,又该在什么时分退后一步进行规划?
当然,这儿搜集到的要求不需求也不或许真实全面。我以为把开发的进程归入规划考量也是彻底可行的,例如:
本地开发将怎么运作?
咱们怎么打包及布置?
咱们怎么进行端到端测验?
咱们怎么对这项新服务进行压力测验?
咱们怎么办理保密信息?
咱们怎么完结 CI/CD 集成?
咱们最近为 BNEF 开宣布一套新的查找体系,这方面作业也给了咱们很大的启示。咱们有必要规划出本地开发流程、考虑 DPKG 办法(打包与布置),一起保证灵敏信息不致外泄。
那么,为什么把保密信息引进出产环境或许引发问题?
咱们不能将其直接添加到代码傍边,不然任何人都可以直接检查。
是否应该将其作为环境变量,好像 12 要素运用所要求的那样?这的确是个好办法,但咱们该怎么完结?(在每次机器发动时都拜访出产设备以填充环境变量,必定是个苦楚的进程。)
将其布置为保密文件?那么该文件来自哪里?又该怎么填充?
终究,整个进程当然不或许手动完结。
总而言之,咱们运用了具有人物拜访操控机制的数据库(只要咱们的机器以及咱们自己可以与该数据库通讯)。咱们的代码会在发动时从该数据库处获取保密信息。这部分信息可以在开发、beta 测验以及出产环境之间顺畅仿制,且各自保存在对应的数据库傍边。
这儿要再提一句,AWS 等各家云服务供货商供给的详细计划或许有所差异。咱们不用为保密信息费多少心。获取人物账户、在 UI 傍边输入保密信息,然后即可保证代码在需求时获取其内容。这些服务可以显着简化整个流程,但之前的探究也并没有白搭——我很快乐自己可以真实了解并赏识这种简练的处理计划。
在规划傍边考虑保护要求
规划体系令人兴奋,但保护呢?恐怕就没什么成就感可言了。
在保护体系的进程中,我想到了这样一个问题:咱们为什么要进行体系降级,又该怎么完结体系降级?
榜首部分的答案是,由于总有人不爱丢掉陈腐的部分,而是添加新的部分。厚古而薄今,至少我自己就有这样的缺点。
至于第二部分,答案是咱们在进行体系规划时提出的终极目标,后续或许不再适用。在体系的开展傍边,其很或许会以与规划假定相抵触的办法进行运用,这意味着咱们最初做出的悉数预期需求都不再有用。这时分咱们就需求撤退一步,层层剥离那些不再适用的部分。
现在,我至少知道三种可以下降降级率的办法。
保证业务逻辑与根底设施互相别离:一般来说,需求降级的往往根底设施部分——例如运用量添加、结构过期、呈现零日缝隙等等。
环绕保护需求规划流程。对新代码与旧代码选用相同的更新手法,然后避免新旧之间呈现差异,保证代码全体坚持“现代”特性。
一直坚持去掉悉数不需求的 / 陈腐的代码。
布置
我更倾向于把功用绑缚在一起,仍是逐个进行布置?
这要取决于现有流程,但假如答案是绑缚布置,那么很或许会引发后续问题。
这儿咱们需求答复的问题是,咱们为什么要把功用绑缚起来加以布置?
是由于布置需求消耗太多时刻吗?
是由于代码检查比较困难吗?
不论是由于什么原因,咱们都需求处理瓶颈自身,而不是在布置办法上做出姑息。绑缚办法至少会带来以下两大坏处。
假如其间一项功用出了过错,就会阻挠另一功用的履行。
这会进步危险水平,或许说导致发作问题的机率上升。
接下来,不论咱们挑选哪一种布置流程,各位必定是期望自己的机器能像耕牛相同兢兢业业,而不是像宠物那样动不动耍脾气。机器有必要吃苦耐劳,咱们知道每台机器上运转的是什么,在宕机时又该怎么康复。一旦发作宕机,咱们不会感到懊丧——发动一台新的就行。这些设备应该像放养的牛羊,而不是需求精心呵护的小猫小狗。
呈现问题时
一旦出了问题——并且迟早必定会出问题——咱们的黄金规律便是尽或许下降对客户形成的影响。
在呈现问题时,我的榜首反响便是处理问题。但事实证明,这并不是最高效的应对思路。相反,即便仅仅小小的问题,最高效的办法其实是挑选回滚。回来之前可以正常作业的情况,这样才干缩短客户无法正常运用服务的时刻窗口。
也只要这样,咱们才干安心查找过错并着手加以修正。
正如集群中的“毛病”机器相同,在测验判别机器出了什么问题之前,咱们首要应该将其下线并标记为不可用。
我发现这的确是种反直觉的办法,并且我的天性总会把自己带离最佳处理途径。
我觉得正是这样的天性,强逼我走上处理 bug 的绵长路途。有时分,引发问题的本源便是我编写的代码出了问题,而我会深化研究自己写下的榜首行代码。这有点像深度优先查找的进程。
假如终究证明是装备发作了改动,而我没能及时调整功用自身,我就会十分气愤。由于这个过错太初级了,本不该发作。
从那时起,我的心得便是在深度优先查找之前先来一轮广度优先查找,暂时不触及尖端节点。我能运用自己手头的资源承认哪些问题?
机器还在运转吗?
装置的代码是否正确?
装备是否到位?
代码是否运用到特定装备,例如代码中的路由是否正确?
架构版别是否正确?
终究,再看代码内容。
咱们本来以为是 nginx 在机器上没有正确装置。但事实证明,仅仅装备文件被设置为 false。
当然,大多数情况下并不需求这么费事。有时分,单靠过错音讯就足以帮我快速找到存在问题的代码。
当我找不出问题时,我会测验分步对代码进行改动以查找或许的本源。改动的数量越少,找到真实问题的速度就越快。总归,请尽或许让推理进程变得有迹可循,过分跳动只会失去头绪。我现在还记住自己曾花了一个多小时处理几个 bug:问题在哪?一般都是我忘了检查的一些初级问题,例如设置路由、保证架构版别与服务版别匹配等等。这只能阐明我对自己运用的技能仓库还不行了解,因而需求经过犯过错的办法堆集阅历。终究,我可以单靠直觉就判别出为什么代码没能正常运转。
战役故事
一边是调整参数与检查统计数据,另一边是修正底层问题本源。
假如没有战役故事(war story,指一段令人难忘的阅历,往往触及危险、困难或许冒险要素),这篇文章又怎么会完好?我很喜爱回忆这类阅历,共享环节立刻开端。
这是个关于查找与 SQLAlchemy 的故事。在 BNEF,咱们需求处理许多由分析师们编撰的研究陈述。每逢陈述发布时,咱们都会收到一条音讯;在收到音讯之后,咱们会经过 SQLAlchemy 进入数据库,获取咱们需求的悉数信息,进行转化,并将效果发送至 solr 实例进行索引。但这时分,咱们发现了古怪的 AF bug。
每天早上,衔接数据库的操作都会失利,音讯提示“MYSQL 服务器不存在”。有时分连下午都会呈现这种情况。由于下午时段的运用量最大,所以我首要进行了一番检查。没问题,机器的运转情况悉数正常。咱们全天会向数据库宣布数千次恳求,都没有失利。那么,为什么负载强度这么低的情况反而会出问题呢?
哦,或许是咱们在业务完毕后没有封闭会话?所以失利其实来自同一段会话,只不过下一个恳求呈现在很长一段时刻之后,这就引发了超时——由于此次服务器现已封闭了。快速检查代码,咱们经过上下文办理器检查了每一次在exit() 上调用 session.close() 的读取操作。
经过一整天的排查,没发现任何问题。在第二天早上,我又遇到了相同的情况。过错发作的一秒之后,其他三项索引恳求都成功了。这显着便是会话未能正确封闭的典型体现。好了,信任咱们可以脑补出接下来的完好故事。
SQLAlchemy mysql 语言中的 Session.close() 无法封闭底层数据库衔接,除非运用 NullPool。是的,这便是修正计划。
引发这个 bug 的原因很简略,这是由于咱们不会在夜间以及午饭时段发布研究陈述。此外,咱们也吸取到另一个经验——大多数仓库溢出问题的答案(我是从谷歌上查来的),正是 bug 自身会调整会话的超时时刻,或许操控每条 SQL 句子所能发送数据量的参数。这些对我来说都没有含义,由于它们与问题的本源无关。我检查了查询巨细是否在约束规模之内,并且由于会话自身正在封闭,所以也不会发作超时情况。
咱们当然可以把超时时刻从 1 个小时添加到 8 个小时来快速“修正”这个 bug。但这明显处理不了问题,到第二天早上,又会有研究陈述引发的过错呈现在咱们面前。
一边是调整参数与检查统计数据,另一边是修正底层问题本源。这便是咱们的日常日子。
监 控
我之前历来没想过监控也会归自己管。坦白讲,在承受全职编码职位之前,我历来不论体系保护这些事。我仅仅构建体系,用上一个礼拜,然后再换一套体系。
现在,我日常运用的是两套体系,其间一套具有杰出的监控机制,另一套的监管机制则比较差。经过实践体会,我感触到了监控的重要含义。终究假如意识到问题,我又怎么能处理问题呢?最差的情况,便是连客户都发现 bug 了,我自己还蒙在鼓里。“我在做什么?!我连自己的体系出了问题都不知道?”
我以为监控机制首要包括三大组件——日志记载、目标与警报。
日志记载以代码的办法存在,相似于人类记载,这是一种渐进的进程。
咱们可以找到需求监控的内容,记载这些内容,一起运转体系。跟着时刻的推移,咱们或许会发现自己短少某些处理 bug 所需求的信息。这正是调整日志记载的好机会——咱们忘了记载哪些重要的内容?
我以为,最重要便是直观地了解哪些内容值得进行记载。作为我的调查目标,他(标题中的高档软件工程师)和我在记载服务方面的主意有着很大的不同。我以为记载恳求 - 呼应就足够了,但他却列出了许多目标,比方查询履行时刻、代码中的一些特定内部调用以及何时轮换日志等等。很显着,假如没有日志记载作为参阅,咱们简直不或许进行任何调试作业——假如咱们不清楚体系的当时情况,重建体系天然也就成了痴人说梦。
目标可以从日志傍边提取,也可以在代码傍边独自树立。(例如将事情发送至 AWS CloudWatch 以及 Grafana)。咱们可以自行设定目标,并在代码运转时宣布对应的数字。
警报则是将悉数内容整合在优异监控体系中的重要粘合剂。假如某项目标代表着当时正处于出产情况的机器数量,那么这个数字下降到 50% 则代表着一种严峻警报——必定是出了什么大问题。失利计数超越栽个阈值?又会有新警报给咱们宣布提示。
这样我就能安心睡觉了,由于我很清楚即便出了什么问题,体系也会立刻提示我~对吧……
而这中心又躲藏着另一种重要的习气。在修正 bug 时,咱们不该单纯重视怎么处理问题,而是为什么咱们没能早点发现?警报有没有及时提示?怎么更好地设置监控以避免呈现相似的问题?我到现在也没弄了解怎么监控 UI。现在的组件选项还无法了解问题终究来自哪里。并且,仍有适当一部分问题是由客户上报过来的——这儿头必定还有提高空间。
总 结
曩昔一年以来,我学到了许多。在开端编撰这篇文章时,我很快乐自己承受了这份新的作业。动笔的进程中,我也殷切体会到自己的生长。期望咱们也能从这篇文章里取得一点启示!
我十分幸运地加入了一支优异的团队——咱们完结了许多编码作业、咱们每天都过得很高兴、咱们从零开端规划体系,咱们也与许多其他团队携手协作。
本年,我身边又多了一位高档开发人员。我很等待能学到更多重要的心得。多谢啦,我的团队!
优异的工程师可以规划出更强健且更易被别人了解的体系。这将带来乘积效应,协助搭档们更快更可靠地构建他们的作业效果。- 怎么构建杰出软件(How to Build Good Software)
我也不承认的那些事儿
我还没有测验过对软件工程代码进行破解。这也提示我,还有许多重要的常识需求学习!假如生长顺畅,下一年的新版别应该会更长。好了,总归先进入现在的待了解问题清单:
应该安身笼统视点考虑,仍是安身形象视点考虑?
我关于干事的办法具有清晰的见地吗?有哪些是犯错之后才总结出的办法?我是否完结过有必要具有这种见地才干处理的使命?
为作业流拟定开发流程。假如咱们由于紧急情况或许事情而有必要改动自己的作业办法,那么这一流程是否会受到破坏?有没有处理办法?
什么样的代码应该被放进 utils 文件夹(专门用于放置不知道该怎么处理的东西)?
怎么处理编码与作业流文档?
怎么监控 UI 以发现异常情况?
花时刻规划出完美的 API/ 代码契约,仍是重复测验加重复迭代,然后找出哪种办法更好?
选简略的办法,仍是选正确的办法?我不信任简略的永久正确,这有点太达观了。
自己着手干事,仍是教会其他不明白的搭档怎么处理?前者速度更快,后者则能一了百了地下降作业量。
重构以及避免进行大规模更新:“假如我改动了整个测验流程,那么或许需求一下替换 52 个文件,这明显会引起严重影响。可是,受到影响的仅仅代码,测验更新悉数顺畅。”这样的价值,值得吗?
进一步下降危险。有哪些战略可以下降项目的危险?
有哪些有用的需求搜集办法?
怎么下降体系降级率?
https://neilkakkar.com/things-I-learnt-from-a-senior-dev.html
点个在看少个 bug