Giter Club home page Giter Club logo

Comments (1)

EthanLin-TWer avatar EthanLin-TWer commented on May 31, 2024

本文首发:https://discussions.youdaxue.com/t/classic-arcade-game-es6-tdd/36499 。欢迎转载,注明作者与出处即可。后续文章更新以 我的 Github Issue #141 为准。文章无法同步更新,请见谅。

项目地址:

文章目录

  1. 回顾 Preface
  2. 极致的开发体验 Superb Developer Experience
    • 触手可及的任务列表管理 GHI + Issues: Tasking List Management in Hand
    • 持续交付的最后一公里 Travis Dashboard: One Last Mile of Continuous Delivery
    • 提交、样式检查自动化 Git Hooks + ESLint: Automated Commit Message & Styling Check
    • 模块化的系统 ES6 Modules: Moduralized Units
    • 小步前进的测试驱动开发 Mocha/Chai/Sinon: Baby Step Test Driven Development
    • 刻意练习,持续进步 Toggl: Measurable Deliberated Practice
  3. 项目完成概览 Overview of Project Accomplishment
  4. 如何使用 TDD 完成代码 TDD in JavaScript
  5. 刻意练习 Deliberate Practice

回顾 Preface

咳咳,上回我们完成了 Udacity 的第一个代码作业,同时也留下了一些痛点没有解决,这篇文章,我们将着重解决这些痛点:

  1. ES5 需要手动实现一些语言层级的特性,比如继承
  2. 没有模块化,导致了无法 TDD
  3. 没有单元测试,从而无法 TDD,无法重构
  4. 没有持续集成流水线,不能提供每一次构建的极速反馈
  5. 没有样式自动化检查,从而样式只能人肉手验

以上几个问题,其实提的都是团队开发规范的问题。在公司的大项目中,由于要协调多人的团队开发(说白了就是对人布朗运动的不信任),我们需要一些约定和规范,比如每个方法代码不能超过多少行、循环不能超过多少层,等。这里,我们更多是使用这些工具 来增强开发体验,把尽可能多的工程活动(流水线、checkstyle、单元测试)都自动化起来,通过它们来提供 快速反馈强化开发信心

以上问题的解决方式分别为:

  • 1和2的解决:使用 ES6
  • 3的解决:引入自动化测试工具 mocha/chai/sinon
  • 4的解决:引入持续集成流水线工具 Travis
  • 5的解决:引入样式自动化检查工具 ESLint

极致的开发体验 Superb Developer Experience

触手可及的任务列表管理 GHI + Issues: Tasking List Management in Hand

这是我新发现的工作流,尚不是很成熟,但令人眼前一亮。它主要解决的问题是更随手可得的 tasking 列表管理。开始每项工作之前,我们一定要分解出一个任务列表,而这个任务列表,你要保持更新,做完了一项要从任务列表中删掉;同时,这个任务列表一定需要更触手可及。前面的文章 是直接编辑 Github Issue,这样既分散精力, Github 也不是一个好的编辑器。如何让 Github issue 更触手可及呢?我想到了命令行。记得之前前端早读课推送过一篇文章,提到 ghi 这个工具,我就一搜,发现这是一个完美的 Github issue 命令行工具。

使用了 ghi + Github issue 来描述任务列表以后,这个工作流就变成:

  1. 先做 tasking,分解出一个任务列表
  2. 把这个任务列表使用 ghi open <issue title> 挨个变成 Github issues
  3. 在命令行使用 ghi list 查看 open 的任务列表,选取一个进行工作
  4. 在提交时,使用对应的 issue number 写入提交信息
  5. 做完一个任务以后,使用 ghi close <issue number> 把任务关闭掉
  6. 重复3-5,直至所有任务做完

image

image

如上,Github 上(或在其他地方)分解出来的任务列表,最后变成 Github issues,被 ghi 工具在命令行管理起来,并且支持 新增 ghi open、查看ghi list、关闭ghi close 这三个简易的管理操作,从而抛弃了 Github issues 的 GUI 界面,使用了终端作为沟通工具(我为命令行设置了一个全局快捷键 Shift+delete 一键打开)提升了效率。

持续交付的最后一公里 Travis Dashboard: One Last Mile of Continuous Delivery

image

image

持续交付(Continuous Delivery)的目标是,每一次提交、构建都是完整可交付的产品代码。实现上,大多 CI/CD 工具都提供了监控界面,这样我们可以快速看到某次是失败还是成功,了解产品的健康状况。为什么我们提倡保持每次提交的代码都是 ready for production 的状态,并且任何时候提交挂了就要马上修复呢?这样一方面可以让我们对产品保持信心,一方面也是让软件开发变得更简单,降低了调试成本。试想,如何一个构建最近20次都是挂的,你怎么知道导致构建失败的问题是什么呢?你怎么知道这20次提交中有无引入新的 bug 呢?你又怎么知道新增的代码有无测试和代码检查的覆盖呢?如果允许一个产品长期是挂球的状态,长此以往必然使开发对产品、对日常开发失去信心,充满沮丧。反之,如果我们保持流水线每次都是绿的,那么即使某一次提交把它挂掉了,我们也能很快找出这个提交、定位问题,很快地修复问题,从而对维护代码的质量起到正反馈的作用。

这里我使用的是 Travis Pipeline,它是对个人免费的产品,并且配置简单,界面相当友好。可以看到上面的提交历史中有一次红掉的提交,这样你很快就可以定位到,是 #25 的 issue、添加了 ESLint 的 prefer-const 检查规则后挂掉的,那么十有八九就是样式检查没有过,马上改一下,就可以以极小(10秒到1分钟)的成本修正错误。从这个例子也可以看到,好的提交信息的重要性,它描述了代码做的事情(而不是怎么做),让你一眼就能看懂,从而不需要亲自去看提交的代码才能知道,这也降低了调试成本。提交信息,正是我们下一节要提到的点。

提交、样式检查自动化 Git Hooks + ESLint: Automated Commit Message & Styling Check

image

提交信息也是一门小学问。好的提交信息可以简易地代替代码阅读,让你就像在读小说一样读代码库,找 bug 的时候(什么?你问有了上面的持续集成/交付(CI/CD)实践为什么还需要找 bug?这是一个好问题!)也可以通过阅读提交信息来快速定位可能有问题的提交。另外,当团队大了以后,每个人可能有不同的提交信息书写习惯,此时团队间统一提交信息格式就尤为重要。即使是一个人的项目,强制规范提交信息也是有必要的,这不仅有益你养成良好的小步提交习惯(大步提交,提交信息必然无法写好),而且也是程序员的自我修养。

在 Udacity 的项目中,官方也有一份 Git 提交信息样式指南,其中的前缀规则非常有用,我已经用到我的项目上。现在,我自己的提交规则是:

  • 必须有自己的名字
  • 必须有 issue number(有需求就建 issue,没需求就不做卡)
  • 必须有 前缀(feature/refactor/fix/chore/style/docs 其中之一,参考 Git 提交信息样式指南
  • 前缀后面必须有冒号
  • 冒号后面必须有一个空格
  • 空格后面必须小写开头
  • 必须不多于70个字符

比如下面就是一个符合提交规范的提交信息:

[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_regexcommit_msg 都设成最简单的 Linesh,然后再一边加 []#()等这些特殊符号,看看它们需不需要被转义。并且最初是另外写了个单独的 bash 文件单独运行快速调试的。这样一步一步把 commit_regex 这个正则试出来以后,copy 到 commit-msg 里面发现居然还不 work!最后只得去看官方文档,也才发现 $1 这个参数传进来的是 .git/.COMMIT_MSG 这个容纳了提交信息的文件名,而非提交信息本身,你还必须 commit_msg=$(cat $1) 才能拿到提交信息。总体上说,这是搭建开发环境时比较耗时的一个部分。

image

Udacity Styleguide 里有一条,函数声明后面不要有分号 ;,而其他所有语句包括变量声明等后面都需要分号 ;,怎么一口气把它们全找出来?还是通过一些样式自动检查的工具,比如 JSHintESLint 等工具。自动化起来还有一个好处是,你不需要在大脑中再开一个“进程”来记忆它,也不需要手动来寻找,这样非常耗费宝贵的时间,工具可以自动帮你找出所有不合规范的地方。如果把样式检查一起配置到 CI 上,每次不合规范的提交都会把流水线挂掉变红,你就会第一时间得到通知,马上去修复。如上图,它提示了说有7个地方该加分号没有加(Missing semicolon)。

模块化的系统 ES6 Modules: Moduralized Units

没有模块化是 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"
  } 
  ...
}

小步前进的测试驱动开发 Mocha/Chai/Sinon: Baby Step Test Driven Development

image

image

有了模块化,有了测试工具,再加上一纸任务列表,我们终于可以进行 TDD 了!简而言之,TDD 是一种测试先行的方式,也即你先写一个测试来描述你的意图,那么测试必然会挂,然后你再通过最快最小的产品代码来实现需求,让测试通过变绿。最后,在测试的保障下,进行必要的重构,消除代码的坏味道。 TDD 是一种设计工具,是一种编码的方法论。它能带来的好处有:

  • 驱使你思考代码的设计
  • 做正确的事:也即只有任务列表的需求才会去写测试和实现
  • 提供极速的反馈:写完一个测试或实现,马上就能看到是挂还是没挂
  • 提供重构的信心:有了测试覆盖和小步前进,重构和设计再也不是艰难的事情,而是愉快的

image

TDD 如何保证你做完了正确的事情呢?换个问法,你怎么知道你做完了 rubric 上声明的所有需求了呢?有同学可能会说,玩一下游戏不就知道了。也没错,不过缺点是需要手动测,并且往后每动代码就必须回归全测一遍,慢。也有同学会说,依据就是前面的任务列表呀,任务列表做完了,我就很确定所有的需求都做完了,因为我的任务列表完整、穷尽地覆盖了 rubric 上所有的需求。很好,思路是对的。我们 tasking 出来的任务列表最后会变成一个个的测试用例,那么,如果所有的测试用例都实现了,同样也证明我的任务列表完全实现了,也就等价于需求完全实现了。测试用例实现没有,这个就非常可视化了,见下图,1秒证明我实现了所有需求,并且自动化的单元测试可以在以后回归的时候重复多次地运行,成本极低。

image

关于 TDD 的深入论述和实践,可以参考上篇提到的一些资料。这是额外的话题,有兴趣深入、了解、质疑的同学欢迎加我微信或群里讨论哈~

刻意练习,持续进步 Toggl: Measurable Deliberated Practice

image

image

image

Toggl 是我使用的一个计时工具。为什么要对任务实现计时呢?如果有同学戳进去了上面👆的那篇编程的精进之法,就会看到作者对刻意练习的观点:通过预估用时 - 实际用时的对比来定位实际耗费过多时间的瓶颈所在。Toggl 可以对整个项目的完成时间做一个记录。当然,类似的计时需求可以通过 IDE 自带的 time tracking 功能来做到,都是可以的。同学们有什么更好的工具也欢迎来分享。

项目完成概览 Overview of Project Accomplishment

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 in JavaScript

突然觉得这部分没什么好说的,TDD 怎么做就是怎么做,说了似乎就变成纯 TDD 贴了。有同学可以给点建议写什么吗?或者,有兴趣的同学可以看一下我的 PR 和提交历史,非常欢迎你的反馈!

#30

刻意练习 Deliberate Practice

  • 发现一个问题:使用了 TDD 实现核心功能使用了209min,不使用 TDD 实现核心功能使用了235min,感觉差别似乎不是特别大,而且后面实现是在已有前面理解的基础上。不过 TDD 从心理上,感觉对项目的信心倍增,无论是功能还是重构,主要还是样式检测和测试自动化在背后的支撑
  • 基础设施的设置则需要更多的时间,差不多需要3个小时。可以看到主要是在 Git Hooks 的配置上花费的时间,背后又是对 bash 的不熟悉
  • Tasking 时间减少了,而且 task 的总体质量还不错。大🐻说,这个例子的逻辑相对简单,建议可以多做些逻辑更复杂的任务分解练习
  • 把骨架代码重构成 ES6 意外顺利
  • #9 #10 #11 #17 #18 #25 花费的时间比预期多了,需要看下提交历史,看是意外的 debug,还是有没预料到的任务
  • Git hooks 上花费的大量时间本质上是 bash 不熟悉的原因
  • 对比一下项目功能完成与重构时间:上次训练,完成项目代码 115min,重构 98min,总用时 213min;这一次,完成项目核心功能 186min,重构 25min,总用时 211min,并没有太大变化。但反映出一个问题:对重构的不熟练,及对 JavaScript 重构的不熟练。可以产出刻意练习计划

下次目标:核心功能总用时进3小时;基础设施代码搭建进1个半小时

刻意练习计划:

  • 特定的重构手法
  • 基础设施 和 核心代码构建 分开训练

from frontend-nanodegree-arcade-game.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.