在Naive Systems实习的这四个月

前情提要:笔者就读于华东理工大学计算机科学与技术专业,即将毕业。

前言

  时值21年2月中旬,此时春节假期刚刚过去,距全国硕士研究生统一招生考试初试也已经两月有余。由于在初试中数学科目失利,一方面需要花一些精力准备复试,另一方面也要考虑到存在没有通过初试的风险,在焦急等待着复试分数线的我决定开始寻找一份能让我在大四下学期即不太忙(以至不影响复试的准备与后续的毕设工作)也不太闲(以至在未能进入复试的情况下能为这一学期的生活托底)的实习工作。

  综合考虑可以接受的地理位置、实习时长与薪资,在实习僧平台上过滤检索出符合条件的岗位后,一共向五个企业发出了求职申请。在一周内拿到了两个前端开发实习的Offer(投的后端实习岗位全部挂在简历关上,反思下来感觉可能是Python在后端开发领域确实不怎么受待见吧):一个是生物制药相关的IT系统开发,另一个则是基础软件开发。其实从工作内容上来看,我最初并没有什么倾向性,但是考虑到前者不包午饭所带来的额外支出(而且工作地点还是在环球港这样的高消费水平商圈),而且不想再浪费时间继续投简历面试,最终还是选择了后者。

  就这样,21年3月1日,我来到零号湾,开始了在Naive Systems为期四个月的实习生活。

零号湾的门卡

第一个月

1
2
3
4
55 Patchsets Created/Updated
12 Patches Merged
17 Patches Abandoned
(cr/158 - cr/227)

Programs must be written for people to read, and only incidentally for machines to execute. ―― Harold Abelson, Structure and Interpretation of Computer Programs

  入职后见到的第一位同事(后来才知道是我的Manager,前后端全栈巨能肝的大佬Nagi)上来先花了半个小时向我介绍了公司办公室文化平等开放包容的特点,强调了交流的重要性,并旗帜鲜明地表达出对于办公室政治的厌恶(听上去似乎是有过非常糟糕的相关经历),随后给了我一个简单的Reading List,其中包含了在开发过程中我所必须掌握的内容。

  我的工作职责主要是在Gatsby/React框架下编写TypeScript代码以实现前端逻辑和样式。所幸由于先前参与过以react-native为基础的商业项目,在交互逻辑的实现上基本没有什么需要现学的内容。但是之前确实没有过参与大型项目,也没有过互相进行代码审核的经历,因而写出来的代码比较随性,对于提交、分支等概念的认识也比较肤浅。因此,第一个月的主要工作内容是熟悉项目源码结构以及适应公司的团队协作工作流,并逐渐培养出良好的编码习惯。

  实习形式的选择 自实习伊始,我就被询问,是更倾向于完成一个独立的较为复杂的功能或者项目,还是倾向于在已有的项目代码上到处打补丁。我更倾向于后者,因为作为实习生来说,前者的成品很可能在实习结束后就被废弃,而后者的情况下提交的改动会在代码库中留存地更久一点。而且后一种方式也会迫使自己尽快熟悉公司已有代码的结构和特点,以使得自己的提交能够更快地通过代码评议。

  Merge工作流与Rebase工作流 在软件工程课上,我所参与的五人小组也曾使用Git作为版本控制工具进行协作开发。当时,由于缺乏对于工作流的清晰认识,只是照着手册上的说明与对多分支开发的模糊理解,下意识地使用了Merge工作流。修改代码,创建提交,然后推到远程分支。如果发现冲突了,则先拉取远程分支的代码,手动Merge一下,再推上去。尽管这样的工作流在功能性上并没有任何问题,但后果是主分支的提交记录非常的“脏”——有一小半内容都是没有什么实际价值的Merge Commit。Rebase工作流的优点是能够在主分支上保持清晰的、线性的提交记录,而且不需要创建Merge Commit。然而对rebase指令的不合理使用”can be potentially catastrophic”。需要注意是,不应该将本地主分支上的提交rebase到其它本地分支的顶端。这会导致本地主分支的历史与其他开发者不同。此时,对本地于远程主分支进行同步或者说合并就会非常麻烦而且令人困惑。不过,只要遵守上述规则,在熟悉之后也会觉得Rebase工作流实际上非常直观易懂。后来,我也从在其他企业实习的同学那里得到确认,在实际工程项目中,主要使用的还是Rebase工作流。

  第一个月的工作内容比较杂,具体包括:给一部分页面加上i18n (cr/158, cr/159),修改CR列表页面的样式 (cr/162, cr/173),修改CR页面的样式 (cr/177),增加客户端命令提示面板 (cr/187),在评论文本中识别URL并自动转化为超链接 (cr/190),令回复按钮在点击后不重复响应用户点击 (cr/195),高亮行末空格 (cr/201)、为后端容器编写测试用例 (cr/205,未完成) 等。

第二个月

1
2
3
4
45 Patchsets Created/Updated
15 Patches Merged
22 Patches Abandoned
(cr/230 - cr/277)

Beware of bugs in the above code; I have only proved it correct, not tried it. ―― Don Knuth, The Art of Computer Programming

  与第一个月相似,第二个月的大部分时间也主要在现有项目中实现各种功能。但从第二个月的下旬开始,我被安排了一项(几乎是)独立负责的工作:用前端框架重新实现主站的界面原型。由于我并没有从零开始建立Gatsby项目的经验,于是又跑去啃官方文档。好在啃到一半时Nagi来帮我初始化了项目的目录结构和基本框架,使得我避开了很多本可能会遇到的坑,也了解到许多构建产品级前端项目时需要关注的细节。

  界定影响的边界 在本月的工作过程中,我负责实现的按C键对选中行进行评论功能存在一个明显的BUG,但是在自己开发测试时并没有发现,改动内容也顺利通过了代码评审,直到部署上线连接到生产环境后被其他同事触发时才被发现。具体来说,由于忽略了事件冒泡所产生的影响,对于按键事件的捕获会在本应停止时依然发生,一方面会出现非预期的行为,另一方面原本试图输入的内容也会收到影响。虽然这个问题在发现后很快就被用一行代码修复了,却也警醒了我,在为复杂系统增加功能时一定要先考虑清楚修改所带来的影响的边界。

  本地多分支开发与本地单分支开发 在第一个月中,我的开发方式主要是本地多分支开发,即以远程主分支作为唯一上游分支,开出多个互相独立互不影响的分支。这种开发方式的优点是同时进行的多个工作内容不会因为代码审查互相阻塞,缺点则是需要思考并维护各个分支的无关性,而且本地开发分支的进度常常会被阻塞,否则在不同分支的代码进入主分支前可能需要自己再手动在本地合并各个分支之间的冲突。但是在后来,Nagi向我介绍了一种更加高效的开发方式,本地单分支开发,也即把所有功能分支以单链的形式挂在远程主分支之后,需要提交时再cherry-pick拆成小分支,或者直接分段提交。优点是由于开发时不用考虑维护分支之间的冲突,本地开发效率会得到很大的提升,而缺点也很明显,原本本地开发效率上存在的阻塞过程会转移到代码评议的过程中(因为提交的Patchset之间可能会存在依赖关系,因而某个Patchset的修改会使之后的所有Patchset全被阻塞)。

  这个月的具体工作主要包括:为Api层调用加入上下文以捕获异常 (cr/230),按C键在选中行上评论 (cr/232),储存列表的折叠状态至本地 (cr/240, cr/265),绘制出组件的基本样式 (cr/258),将页面原型用react实现 (cr/275, cr/277) 等。

Micro Kitchen一隅(甚至让我一度以为公司里有位俄罗斯程序员)
# 第三个月
1
2
3
4
29 Patchsets Created/Updated
25 Patches Merged
12 Patches Abandoned
(cr/283 - cr/352)

What you see is all you get. ―― Brian Wilson Kernighan

  从第三个月开始,主站界面的构建与样式调整成为了我的主要工作内容。虽然很早之前就了解过React/Material-UI中使用样式的方法,也系统学习过CSS,但在实际日常应用中由于通常是直接使用了带样式的组件库(比如antd),对于样式和布局相关细节的记忆早就模糊了。因此在这个月里也读了许多和Flex、Grid等布局方式以及各种样式生效原理相关的文档。

  样式调整的法则与布局的逻辑 首先是尽量不要用内嵌样式。内嵌样式会提高代码的耦合性,让样式的变更变得难以维护。接着,尽量不要使用高阶组件。高阶组件的主要缺点是,难以从外部变更组件的样式(会被组件内部行为覆盖)。另外,相同或相关的参数应该尽量转移到主题选项里,比如spacing和color等属性。在布局上,应该优先倾向于选择FlexBox,其相对于Grid或者Table都更加灵活(当然,其布局逻辑也因此略显复杂)。在布局的过程中,也尽量少用固定的长宽度以使得组件能够根据页面的大小自行调整尺寸,也即充分运用Material-UI/CSS所具备的Responsive特性。

  前后端分离的影响 引入前端框架通常意味着前后端分离开发的开始。尽管能对系统结构进行有效的解耦,前后端分离开发也会引入一些额外的问题,比如跨域请求的处理,前端回调地址的设置,以及不同前端实例需要针对不同后端分别部署等等问题。在开发过程中,曾遇到过一个神奇的案例:某个前端跳转至特定页面的功能在测试环境下有效,但到了生产环境时就失效了,会跳转至一个空白页面。经过排错分析,发现其实是在跳转时使用了前端路由的原因:在开发环境下,所使用的地址会被代理中间件响应,并被正确代理。但在生产环境下,由于使用了前端路由的跳转方式,框架会认为程序试图跳转至相应地址的前端页面(而非直接访问该地址),因而会在前端程序中尝试寻找对应地址的页面,当然就会一无所获而呈现出空白。解决方式也很简单,强制浏览器跳转到该地址即可。这次排错经历提醒了我,在实际开发过程中要仔细考虑前后端分离带来的影响,以及要关注开发环境与生产环境存在的差异。

  这个月的具体工作包括:调整主页前端的各种样式与部分逻辑 (cr/283, cr/295, cr/323, cr/324, cr/346, cr/352)与一些功能的修复 (cr/325) 等。

第四个月

1
2
3
4
20 Patchsets Created/Updated
19 Patches Merged
1 Patch Abandoned
(cr/360 - cr406)

It’s not only the dev schedule either. Don’t believe anything dependencies say――ever. If they are in the next room and tell you it’s raining, check your window first. ―― Eric Brechner, I. M. Wright’s “Hard Code”

黄金体验镇魂曲:永远无法达到的真实(截止本文成稿,曾被宣称将在五月初发布的公司主要产品仍处于开发阶段)

  在实习的第四个月,我需要抽出一部分时间来进行毕设论文的编写与答辩,因而留给实习工作的时间也减少了一些。在本月中,我的主要工作内容是继续根据UI/UX提供的样式设计稿与交互流程图来修改主页前端。

  前端开发的本质 在开始实习之前,我曾天真地以为前端开发所要做的工作十分简单,无非让数据在特定的时机出现在合理的位置,其本质上是一个Mapping或者说Binding的过程。但经历了这段时间里在in-the-wild环境下的前端开发工作之后,现在我对前端开发的认识发生了变化:前端开发是人机交互逻辑的实现,一方面需要向UI/UX负责以实现出合理的用户体验,另一方面又要充分考虑机器本身的特性,踏入UI/UX所看不到的、属于机器的隐秘之地(比如解决不同浏览器的各种实现标准带来的差异),因而其工作内容实际上更像是一个协调者的存在。

  控制复杂性 控制复杂性有着多个层面的含义,比如控制功能开发阶段的复杂性,控制程序行为的复杂性,控制程序后期维护的复杂性等等。这些不同方面往往还是互相矛盾的,比如某些代码段可能开发时刻意压行,写起来觉得代码段很短很舒服,但在维护时可能会遇到错误产生的具体位置模糊不清难以追查的情况;又比如实际工程项目中其实是希望变量名称越详尽冗长越好,因为这样会减少和其他代码中的变量同名的概率,能够大大减少维护期的排错成本。一般来说,复杂性不会凭空消失,只会在不同形式之间转移,因此如何进行取舍让复杂性尽可能平摊在各个阶段或是集中在某些阶段就成了非常有价值的问题。对于实习期间我所参与的以服务形式提供支持的软件产品来说,维护阶段显然要长于开发阶段,因而将复杂性从维护阶段转移至开发阶段会是更好的选择。

  这个月的具体工作包括:继续调整主页前端的各种样式与部分逻辑 (cr/360, cr/366, cr/367, cr/371, cr/397, cr/399, cr/406) 等。

总结

  四个月的实习时间转眼间进入了尾声,这段经历也给我带来了许多收获。在实习过程中,我见证并参与了大型软件的开发流程,熟悉了目前最scalable的现代软件开发方式,结识了一群在FAAG待过的经验丰富而老练的开发者,并充分浸润在了工程师文化之中。这些经历都使我对软件工程的理解产生了很大的提升,也远远超过了我对实习的预期。虽说软件工程“没有银弹”,但我相信精心设计的合理的工作流与易用的基础设施能够在某种程度上有助于简化软件开发的复杂性,从而提升软件整体质量。

  这段实习的经历同时也让我充分意识到了自己的编程能力、工作效率和代码习惯与核心开发者之间的差距,并坚定了我继续深造的信念。人一生要工作五十年,拿出几年时间读个研提升下自己的能力和对技术与工程的理解、满足下自己的追求又会有什么损失呢?