Closed EthanLin-TWer closed 7 years ago
本文首发:https://discussions.youdaxue.com/t/classic-arcade-game-es6-tdd/36499 。欢迎转载,注明作者与出处即可。后续文章更新以 我的 Github Issue #141 为准。文章无法同步更新,请见谅。
项目地址:
咳咳,上回我们完成了 Udacity 的第一个代码作业,同时也留下了一些痛点没有解决,这篇文章,我们将着重解决这些痛点:
以上几个问题,其实提的都是团队开发规范的问题。在公司的大项目中,由于要协调多人的团队开发(说白了就是对人布朗运动的不信任),我们需要一些约定和规范,比如每个方法代码不能超过多少行、循环不能超过多少层,等。这里,我们更多是使用这些工具 来增强开发体验,把尽可能多的工程活动(流水线、checkstyle、单元测试)都自动化起来,通过它们来提供 快速反馈 及 强化开发信心。
以上问题的解决方式分别为:
这是我新发现的工作流,尚不是很成熟,但令人眼前一亮。它主要解决的问题是更随手可得的 tasking 列表管理。开始每项工作之前,我们一定要分解出一个任务列表,而这个任务列表,你要保持更新,做完了一项要从任务列表中删掉;同时,这个任务列表一定需要更触手可及。前面的文章 是直接编辑 Github Issue,这样既分散精力, Github 也不是一个好的编辑器。如何让 Github issue 更触手可及呢?我想到了命令行。记得之前前端早读课推送过一篇文章,提到 ghi 这个工具,我就一搜,发现这是一个完美的 Github issue 命令行工具。
使用了 ghi + Github issue 来描述任务列表以后,这个工作流就变成:
ghi open <issue title>
挨个变成 Github issuesghi list
查看 open 的任务列表,选取一个进行工作ghi close <issue number>
把任务关闭掉如上,Github 上(或在其他地方)分解出来的任务列表,最后变成 Github issues,被 ghi
工具在命令行管理起来,并且支持 新增ghi open
、查看ghi list
、关闭ghi close
这三个简易的管理操作,从而抛弃了 Github issues 的 GUI 界面,使用了终端作为沟通工具(我为命令行设置了一个全局快捷键 Shift+delete
一键打开)提升了效率。
持续交付(Continuous Delivery)的目标是,每一次提交、构建都是完整可交付的产品代码。实现上,大多 CI/CD 工具都提供了监控界面,这样我们可以快速看到某次是失败还是成功,了解产品的健康状况。为什么我们提倡保持每次提交的代码都是 ready for production 的状态,并且任何时候提交挂了就要马上修复呢?这样一方面可以让我们对产品保持信心,一方面也是让软件开发变得更简单,降低了调试成本。试想,如何一个构建最近20次都是挂的,你怎么知道导致构建失败的问题是什么呢?你怎么知道这20次提交中有无引入新的 bug 呢?你又怎么知道新增的代码有无测试和代码检查的覆盖呢?如果允许一个产品长期是挂球的状态,长此以往必然使开发对产品、对日常开发失去信心,充满沮丧。反之,如果我们保持流水线每次都是绿的,那么即使某一次提交把它挂掉了,我们也能很快找出这个提交、定位问题,很快地修复问题,从而对维护代码的质量起到正反馈的作用。
这里我使用的是 Travis Pipeline,它是对个人免费的产品,并且配置简单,界面相当友好。可以看到上面的提交历史中有一次红掉的提交,这样你很快就可以定位到,是 #25 的 issue、添加了 ESLint 的 prefer-const
检查规则后挂掉的,那么十有八九就是样式检查没有过,马上改一下,就可以以极小(10秒到1分钟)的成本修正错误。从这个例子也可以看到,好的提交信息的重要性,它描述了代码做的事情(而不是怎么做),让你一眼就能看懂,从而不需要亲自去看提交的代码才能知道,这也降低了调试成本。提交信息,正是我们下一节要提到的点。
提交信息也是一门小学问。好的提交信息可以简易地代替代码阅读,让你就像在读小说一样读代码库,找 bug 的时候(什么?你问有了上面的持续集成/交付(CI/CD)实践为什么还需要找 bug?这是一个好问题!)也可以通过阅读提交信息来快速定位可能有问题的提交。另外,当团队大了以后,每个人可能有不同的提交信息书写习惯,此时团队间统一提交信息格式就尤为重要。即使是一个人的项目,强制规范提交信息也是有必要的,这不仅有益你养成良好的小步提交习惯(大步提交,提交信息必然无法写好),而且也是程序员的自我修养。
在 Udacity 的项目中,官方也有一份 Git 提交信息样式指南,其中的前缀规则非常有用,我已经用到我的项目上。现在,我自己的提交规则是:
比如下面就是一个符合提交规范的提交信息:
[Linesh][#23] Refactor: extract move() method
但是问题来了:你怎么保证你的每次提交都能遵循完全相同的提交格式呢?这不仅要求你对提交规则烂熟于心,而且有时人为的错误(比如打字打错等)更是无法避免的,有没有自动化的方式来辅助检查提交信息呢?当然有。答案就是 Git 原生提供的 Hooks。
Git Hooks 是比较大的系统,这里不深讲因为我也只知道冰山一角,但它的思想在软件工程或库开发中都比较常见。因为库或框架可以复用最基本的工作流,而灵活的定制能力则通过提供前后的拦截器或 hook 来允许用户自己扩展,比如 npm scripts、生命周期(比如 Servlet、React Component 等的生命周期概念)等。我们这里的目标是要检查提交信息的格式,如果格式不正确则拒绝该次提交。这里我用到的一个 hook 是 commit-msg
,它位于 .git/hooks/
文件夹下。它正是允许你在提交前后做一些操作的 hook:
#!/bin/sh
commit_regex="\[Linesh\]\[#\d*\] (Chore|Feature|Fix|Docs|Style|Refactor|Test): [a-z]"
error_msg="Aborting commit, please double check your commit message."
commit_msg=$(cat $1)
if ! echo "$commit_msg" | grep -E "$commit_regex" ;
then
echo "$error_msg" >&2
exit 1
fi
调试这个脚本可费劲了,说到底还是我的 bash 基本功不扎实,基本是边 stakeoverflow 一边调试的节奏。说是如此,还是遵循小步试错的思想来的,比如一开始我是把 commit_regex
和 commit_msg
都设成最简单的 Linesh
,然后再一边加 [
、]
、#
、(
、)
等这些特殊符号,看看它们需不需要被转义。并且最初是另外写了个单独的 bash
文件单独运行快速调试的。这样一步一步把 commit_regex
这个正则试出来以后,copy 到 commit-msg
里面发现居然还不 work!最后只得去看官方文档,也才发现 $1
这个参数传进来的是 .git/.COMMIT_MSG
这个容纳了提交信息的文件名,而非提交信息本身,你还必须 commit_msg=$(cat $1)
才能拿到提交信息。总体上说,这是搭建开发环境时比较耗时的一个部分。
Udacity Styleguide 里有一条,函数声明后面不要有分号 ;
,而其他所有语句包括变量声明等后面都需要分号 ;
,怎么一口气把它们全找出来?还是通过一些样式自动检查的工具,比如 JSHint 或 ESLint 等工具。自动化起来还有一个好处是,你不需要在大脑中再开一个“进程”来记忆它,也不需要手动来寻找,这样非常耗费宝贵的时间,工具可以自动帮你找出所有不合规范的地方。如果把样式检查一起配置到 CI 上,每次不合规范的提交都会把流水线挂掉变红,你就会第一时间得到通知,马上去修复。如上图,它提示了说有7个地方该加分号没有加(Missing semicolon
)。
没有模块化是 JS 一直的痛啊,从语言诞生即如此。我们为什么想要模块化呢?因为这是我们管理一个软件系统复杂性的方法,有了它我们可以分别对每个模块进行单元测试。好在 ES6 之后,标准终于提出了一套实现模块化的规范,只不过最新的 NodeJS 还不支持,因此,我们要使用 Babel 等转译器(transformer)来对使用了模块化的代码进行转义。这里我不多啰嗦了。只需要通过 npm 引入 babel-core 和一些语法 preset 即可,同时测试代码也需要被转译。
.babelrc
{
"presets": [ "es2015", "stage-0" ]
}
package.json
{
...
"scripts": {
"test": "mocha test --recursive --compilers js:babel-core/register"
}
...
}
有了模块化,有了测试工具,再加上一纸任务列表,我们终于可以进行 TDD 了!简而言之,TDD 是一种测试先行的方式,也即你先写一个测试来描述你的意图,那么测试必然会挂,然后你再通过最快最小的产品代码来实现需求,让测试通过变绿。最后,在测试的保障下,进行必要的重构,消除代码的坏味道。 TDD 是一种设计工具,是一种编码的方法论。它能带来的好处有:
TDD 如何保证你做完了正确的事情呢?换个问法,你怎么知道你做完了 rubric 上声明的所有需求了呢?有同学可能会说,玩一下游戏不就知道了。也没错,不过缺点是需要手动测,并且往后每动代码就必须回归全测一遍,慢。也有同学会说,依据就是前面的任务列表呀,任务列表做完了,我就很确定所有的需求都做完了,因为我的任务列表完整、穷尽地覆盖了 rubric 上所有的需求。很好,思路是对的。我们 tasking 出来的任务列表最后会变成一个个的测试用例,那么,如果所有的测试用例都实现了,同样也证明我的任务列表完全实现了,也就等价于需求完全实现了。测试用例实现没有,这个就非常可视化了,见下图,1秒证明我实现了所有需求,并且自动化的单元测试可以在以后回归的时候重复多次地运行,成本极低。
关于 TDD 的深入论述和实践,可以参考上篇提到的一些资料。这是额外的话题,有兴趣深入、了解、质疑的同学欢迎加我微信或群里讨论哈~
Toggl 是我使用的一个计时工具。为什么要对任务实现计时呢?如果有同学戳进去了上面👆的那篇编程的精进之法,就会看到作者对刻意练习的观点:通过预估用时 - 实际用时的对比来定位实际耗费过多时间的瓶颈所在。Toggl 可以对整个项目的完成时间做一个记录。当然,类似的计时需求可以通过 IDE 自带的 time tracking 功能来做到,都是可以的。同学们有什么更好的工具也欢迎来分享。
Lesson | Description | Estimated Effort | Duration | Total |
---|---|---|---|---|
P1 - Arcade Game | 阅读项目要求 | - | - | |
把游戏克隆到本地,用 IDE 打开 #4 | 5min | 4min | 4min | |
Tasking | ||||
任务分解 #2 | 30min | 12min | ||
将任务列表创建为 Github issues #3 | 20min | 7min(spike) + 16min | 35min | |
Infrastructure | ||||
持续集成流水线 Travis #5 | 10min | 6min | ||
提交记录 Git Hook #29 | 50min | 2h(120min) | ||
安装 yarn #6 | 2min | 2min | ||
安装 ESLint #7 | 10min | 13min | ||
安装 ES6 转译器 Babel #8 | 10min | 1min | ||
安装 mocha/chai/sinon #9 | 2min | 9min | ||
运行第一个测试 #10 | 5min | 14min | ||
安装 browserify #11 | 2min | 8min | ||
更新项目的 .gitignore #12 |
2min | 1min | 2h 54min | |
Get the Game Up and Running | ||||
把骨架重构成 ES6代码 #13 | 40min | 16min | ||
把代码 bundle 到 dist 目录,且能在浏览器中运行起来 #14 | 20min | - | 16min | |
Core Features | ||||
player 要能上下左右移动 #15 | 30min | 85min | ||
enemy 也要能以恒定速度移动 #16 | 10min | 5min | ||
enemy 速度可调 #17 | 5min | 14min | ||
能实现碰撞检测 #18 | 20min | 31min | ||
碰撞发生后 player 要复位 #19 | 5min | - | ||
游戏胜利后 player 也要复位 #20 | 5min | 19min | 2h 39min | |
Other Features - Error Handling | ||||
player 不能超出画布 #21 | 15min | 15min | ||
enemy 能穿过屏幕,能循环出现 #22 | 15min | 12min | 27min | |
Styleguide | ||||
看是否还有可重构的点 #23 | 30min | 25min | ||
编写 README #24 | 30min | 1min | ||
样式对齐 #25 | 20min | 37min | 53min | |
8h |
突然觉得这部分没什么好说的,TDD 怎么做就是怎么做,说了似乎就变成纯 TDD 贴了。有同学可以给点建议写什么吗?或者,有兴趣的同学可以看一下我的 PR 和提交历史,非常欢迎你的反馈!
https://github.com/linesh-simplicity/frontend-nanodegree-arcade-game/pull/30
下次目标:核心功能总用时进3小时;基础设施代码搭建进1个半小时。
刻意练习计划:
linesh-simplicity/linesh-simplicity.github.io#141