EthanLin-TWer / ethanlin-twer.github.io

💥学习笔记,React 全家桶 / TDD / JavaScript / 开发者效率 / 敏捷实践 / Udacity / 学习之道 等主题🍺
https://ethanlin-twer.github.io
143 stars 17 forks source link

P1: Classic Arcade Game #138

Open EthanLin-TWer opened 7 years ago

EthanLin-TWer commented 7 years ago

重要的资料

练习点

TASKING


我终于知道这玩意怎么回事了。可以开始列需求了。

EthanLin-TWer commented 7 years ago

用时总结

下面几个图是前端课程中间所经历的一些阶段,除了可以看到交替学习的课程以外,还可以得出下面这个总的时间学习表:

image

image

image

image

Lesson Description Duration Total
- 产出一个学习路线图,发到微信群分享+求大家 review 1:25:14 1h 25min
1.0 JavaScript 预备 18:59 18 min
1.1 作用域 Scope 1:59:56
1.1 搜索相关比喻、阅读资料 2:47:32 4h 47min
1.2 闭包 Closure 39:24
1.2 搜索相关比喻、阅读资料 3:10:04 3h 49min
1.3 this 关键字 53:01
1.3 搜索相关比喻、阅读资料 1:55:00 2h 48 min
1.4 类与原型链 Classes versus Prototypes 25:34 25 min
1.5 装饰模式 Object Decorator Pattern 1:30:31 1h 30min
1.6 函数类 与 原型类 Functional Classes vs. Prototype Classes 27:56 28 min
P1 阅读项目要求 26:34
任务分解 22:19
让游戏先跑起来 26:31
实现基本逻辑 1:23:34
重构代码 1:38:29
理解原型类两次继承原理及实现 2:12:59
添加测试及测试设施 15:03
实现胜利后效果 27:00
撰写 README 18:44
对齐 Styleguide 17:56
作业提交 09:40
使用 Gravatar 链接 WordPress 来在优达中显示头像 23:14
收到 Code Review,修改代码、查阅资料 1:24:14
产出博客 5h 20min 13h 17min
- 前端入门/进阶直播课 1:30:32 1h 30min
- 导师见面准备 1:10:43
- 导师一对一 34:05 1h 45min
EthanLin-TWer commented 7 years ago

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

项目地址:

两个简易的实现教程:

文章目录

  1. 两种重要的思维工具 Two Powerful Mind Tools
  2. 任务分解 Tasking
  3. 关键场景实现 Key Scenarios
    • 让游戏先跑起来 Get the Game Up and Running
    • 碰撞检测与复位 Collision Detection and Game Reset
    • 重构:精益求精的手艺 Refactor: The Craftsmanship
  4. 完成时间分析 Time Entries Analyse
  5. 展望 The Future
    • ES5 的痛点 - ES2015: New Generation of JavaScript
    • 快速反馈 - TDD with ES6 in JavaScript
    • 刻意练习 - Two Times Quicker

两种重要的思维工具 Two Powerful Mind Tools

image

这是 Udacity 前端开发(进阶)纳米学位的第一次作业。作业内容是由一个项目骨架,做出一个能像上图那样运行起来的界面及游戏。文章接下来的部分会分享我是如何分析、完成这个项目的代码的,不过在此之前,我想先分享两个很重要的思维工具——也可称为学习理念和方法——两个能提高编码效率的思想/工具,它同样可以应用在其他项目中,让你的思维变得更加顺畅、没有遗漏。

快速反馈 是我们在编码、学习过程希望能取得的一个效果,即是我每写一行代码,我就能看到它的运行效果,不管是在浏览器中,还是通过控制台输出(导师 Brian 也经常提及)等方式。可能有同学会问,真的有必要每写一行就看一下吗?当然,写多少行代码看一下主要还是看你个人的信心。不过这样小步前进的好处是,每次发现代码不工作,我马上就可以肯定一定是刚刚写的几行代码出了问题,这样就极大地方便了我的调试和验证。保持这样一种小步前进的节奏感,我可以很顺畅很快速地写出代码,并且在后续能够很安全地修改、重构代码。

用于支撑这一思想的工具包含但不仅限于以下工具,接下来我会一一提到:

  1. Udacity Git Commits Styleguide
  2. Git
  3. TDD(测试驱动开发)
  4. CI(持续集成工具,如 Travis)

任务分解 则是(我认为的)一切编程任务的起点。拿到一道题目或一个需求,不要一下子钻入细节、思考设计方案,更根本的工作是:定义需求范围,并且将问题完整、穷尽地分解成一个相互独立的任务列表。它主要解决了以下两个问题:

  1. 你要做什么事:什么是要做的,什么是不做的,要把问题域 完整穷尽独立 地分解成 任务列表。这有一个好处:防止一下子深入细节,觉得这个特性要,那个代码也要重构,结果全都不需要,而主要的特性覆盖则有遗漏。这是它提高工作效率的一个方式:避免无效工作。任务分解要完整及穷尽,这保证了任务列表是需求的等价表述,多做或少做了的任务列表都失去了做任务分解的价值;独立则让你在开发时可以独立、没有依赖地做每项任务,减少互相影响,符合 SRP(单一职责原则)
  2. 你怎么知道你做完了:上一步产出的任务列表便是唯一判断。任务列表全部做完了,我就可以很有信心地说我的需求全完成了;否则便没有。通常,我们会把任务列表编写成可自动化执行的测试,但是在这个项目中我还做不到这个,最后一章会简述原因及解决方案

任务分解 Tasking

image

上小节简单地分享了为何我觉得任务分解是先于编写代码的,以及好的任务分解是怎么样。那么,任务分解这个事情究竟如何来做呢?无他,唯手熟尔(让大家失望了哈哈)。回到这个作业本身,从哪里开始搜集需求呢?那肯定是从作业的官方文档开始啦:

第一篇的文档提到一些资源,比如你遇到困难了可以向哪里寻求帮助啦、一些编写 OO 代码的资料视频啦,这些我们就当种个草🌿,后面遇到困难了就来看。它最重要的其实就两点(第3点和第10点):让你读上面的 第二篇文档 和 第三篇文档😂。

……(经过了一顿分析)

打开上面第三篇文档的时候,这样好的 UI 是很讨喜的,而且我发现这个 specification 已经很清楚地将我们要完成的功能写出来了:

image

其他还有一些要求:面向对象的 JS 代码、编写 README 文档、必要的注释,以及符合 Udacity JavaScript Styleguide 的代码(格式、分号、空格等)。再综合上面第二篇文档(里面的内容比较细节),我们可以整理出这样一份任务列表:

不得不说,一开始我的任务列表也不是完善的,比如我就忽略了虫子要有不同速度这个需求,又比如项目从头至尾都没有测试设施和 CI 设施,所有测试都是手工人肉验证的。这个项目不大,看起来还没什么问题,不过如果以后要演化成 这样,那就是没测试或每改一处就人肉测到死的节奏了,根本不可能实现快速反馈(因为反馈出来的错误可能在1000行代码中,无法定位)。(跑题了)不过我也是在不断练习的过程中,不用追求一次就把任务列表列完整。

另外,这个任务列表其实是有一些想法在里面的,比如:

下面我会挑一些关键的场景来分享。

关键场景实现 Key Scenarios

让游戏运行起来 Get the Game Up and Running

这一步里完成的,其实对应就是 第一个简易教程 里的

这里假设我们都已经把代码通过 git clone 到本地了,并且已经在项目根目录下。首先,我们要做的事是 能够在浏览器访问到 index.html。方法有很多种,比如你可以直接打开项目目录,双击 index.html 打开;这里我选择的是使用 node 的 http-server -c-1 -o 命令直接将整个目录暴露到本机的 8080 端口下,这样我们就可以通过 localhost:8080 直接访问了。具体的配置方法,可以参考 这个 README

服务器启动成功以后,刷新一下,页面都是

image

其中,主要是这两个错误:

Uncaught ReferenceError: allEnemies is not defined                            engine.js:94 
    at updateEntities (http://localhost:8080/js/engine.js:94:9)
    at update (http://localhost:8080/js/engine.js:82:9)
Uncaught ReferenceError: player is not defined                                   app.js:45
    at HTMLDocument.<anonymous> (app.js:45)

好,不就是变量未定义么!分别在 engine.js 的第94行和 app.js 的第45行。我们这就去定义一个,首先看到 engine.js 的第94行:

94 allEnemies.forEach(function(enemy) {
95     enemy.update(dt);
96 });

用到了 forEach,看起来 allEnemies 是个数组,并且数组的每个元素需要有一个 update 方法。看到 app.js 第29行的注释(“请在这里实例化你的对象不,是你的对象\~\~”),这就是我们就在定义变量的好位置:

29 // Now instantiate your objects.
30 // Place all enemy objects in an array called allEnemies
31 // Place the player object in a variable called player
32 var allEnemies = []
33 var player = {}

再刷新一下浏览器,此时我们会看到:

image

很好,前面的错误消失了。这次是说,player 上没有 updatehandleInput 方法。没有那就定义一个!怎么定义呢?想起前面的 enemy 也需要有一个 update 方法了吗?想起了前面的简易教程里提到的做法了吗?赶紧找相似的代码开抄啊!还是 app.js,往上滚几行,看到下面的代码:

2  var Enemy = function() {
3      // Variables applied to each of our instances go here,
4      // we've provided one for you to get started
5 
6      // The image/sprite for our enemies, this uses
7      // a helper we've provided to easily load images
8      this.sprite = 'images/enemy-bug.png';
9  };

11 // Update the enemy's position, required method for game
12 // Parameter: dt, a time delta between ticks
13 Enemy.prototype.update = function(dt) {
14    // You should multiply any movement by the dt parameter
15     // which will ensure the game runs at the same speed for
16     // all computers.
17 };

从这段代码,我们可以获知三个信息:

于是,我们可以 app.js 第24行,注释这段下面添加我们的 Player 代码(咦,发现我们推断出来的信息都在注释里写得很明白了嘛。再次地,这只是我自己的一个思考过程,并且意在展现和交流这种思考过程):

24 // Now write your own player class
25 // This class requires an update(), render() and
26 // a handleInput() method.
27 var Player = function () {
28 };
29 
30 Player.prototype.update = function (dt) { };
31 
32 Player.prototype.handleInput = function (movement) {}

好!离让游戏跑起来又接近了一步!再次刷新浏览器,看看游戏是否跑起来了?……

image

image

瞬间我的表情是这样的☝,错就错了,错误信息竟然都没变。好吧,不怕,我们是小步前进,稍加分析一下就知道了。我们加的代码定义了一个 Player 及两个函数,属添加型变化,应该对现有代码没有影响……哎,对!问题不就在这里吗?我们定义了,但是没用啊!这就清楚了,回去把我们定义的 enemiesplayer 都使用我们创建的函数(对象):

// Now instantiate your objects.
// Place all enemy objects in an array called allEnemies
// Place the player object in a variable called player
var allEnemies = [new Enemy()]
var player = new Player()

再次刷新浏览器💥:

image

不错不错,看到游戏界面了!前进了一大步啊!哎,可是这人和蟑螂(妹子表示其实那个像瓢虫🐞)哪去了?再发现,右边控制台还有一个报错,说是 player 没有 render() 方法。render 不就是“渲染”的意思嘛,看来人没出来是因为没写渲染函数啊!记得上面 app.js 第25行的注释也说,"the Class requires a ... render() ... function",行,有道理有道理,我们没加,回去加个 render() 方法。照抄 Enemyrender() 方法就行:

19 // Draw the enemy on the screen, required method for game
20 Enemy.prototype.render = function() {
21     ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
22 };

37 // Draw the enemy on the screen, required method for game 请原谅我连注释一起抄了,抄的要义就是快嘛…注释里的 enemy 我也懒得改了……
38 Player.prototype.render = function() {
39     ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
40 };

抄完发现了什么?Enemy 需要一个 xy,而现在都还没有呢。掐指一算,那应该就是瓢虫的坐标嘛,同理,Player 肯定也需要一个 xy,还有一个 sprite 变量,看起来是个图片。用 IDE 搜索打开,就会发现图片其实就是虫子和人的图片,初步猜测,它们肯定是会被引擎找到并渲染在页面上。不打紧,先把这些记着,刷新一下页面,看下会发生什么。没准什么都不用加就通过了呢(๑•̀ㅂ•́)و✧:

image

然而,虫子并没有出现,而且,这回控制台出错的信息我是彻底看不懂了。怎么办呢?没关系啊,回去把刚刚发现少的东西加上吧。代码如下,我删了一些注释,由于目标是让游戏能快速运行起来,所以代码质量并没有在意,分号换行什么的此时都不重要,后面可以再修整:

1  // Enemies our player must avoid
2  var Enemy = function(x, y) {
3      this.x = x;
4      this.y = y;
5      this.sprite = 'images/enemy-bug.png';
6  };
7  
8  // Update the enemy's position, required method for game
9  // Parameter: dt, a time delta between ticks
10 Enemy.prototype.update = function(dt) {
11     // You should multiply any movement by the dt parameter
12     // which will ensure the game runs at the same speed for
13     // all computers.
14 };
15 
16 // Draw the enemy on the screen, required method for game
17 Enemy.prototype.render = function() {
18     ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
19 };
20 
21 // Now write your own player class
22 // This class requires an update(), render() and
23 // a handleInput() method.
24 var Player = function (x, y) {
25    this.x = x;
26    this.y = y;
27    this.sprite = 'images/char-boy.png'
28 }
29 
30 Player.prototype.update = function (dt) {
31 
32 }
33 Player.prototype.handleInput = function (movement) {
34 
35 }
36 Player.prototype.render = function() {
37    ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
38 };
39 
40 // Now instantiate your objects.
41 // Place all enemy objects in an array called allEnemies
42 // Place the player object in a variable called player
43 var allEnemies = [new Enemy(1, 2)]
44 var player = new Player(1, 2)
45 
46 // This listens for key presses and sends the keys to your
47 // Player.handleInput() method. You don't need to modify this.
48 document.addEventListener('keyup', function(e) {
49     var allowedKeys = {
50         37: 'left',
51         38: 'up',
52         39: 'right',
53         40: 'down'
54    };
55 
56     player.handleInput(allowedKeys[e.keyCode]);
57 });

此时再刷新一下浏览器,猜猜会看到什么?!

image

💥看到小人和虫子啦!!!这一步任务大功告成!不过为什么都是左上角?尝试多调调 Player 和 Enemy 的位置,发现它们位置的规律,就可以把它们调到正确的位置上啦。在我自己的练习中,也是把人物调好了才进行下一步的。不是必须的,不过接下来对于实现人物上下左右等功能看起来会比较直观一些。

做到这里,我们的任务列表就变成了这样:

接下来我们看下核心功能的实现。

碰撞检测 与 复位 Collision Detection and Game Reset

如何定义核心功能呢?这个看个人的兴趣,并无定式,甚至不定义核心功能也行,反正你需要完成的需求列表都出来了,总工作量是一定的,只是谁先谁后的问题。不过我的习惯是,会定义一个核心功能列表,如何定义这个“核心”呢?核心即是价值最高的代码,无论是业务代码还是技术代码。比如说,对于保险公司来说,核心代码就是它的计价引擎,对于支付宝等业务来说,支付就是它的核心代码(直接挣钱的代码)。在这个例子中,核心代码我认为是两个部分:人物移动碰撞检测。当然,价值的认定是很主观的,还是依赖大家的习惯、思考与直觉。

人物与动物移动部分的代码不难实现,我在这里就暂且略过不表。主要的工作是:

11 Enemy.prototype.update = function(dt) {
15    this.x += dt * this.speed
16 };

35 Player.prototype.handleInput = function (movement) {
36    switch (movement) {
37       case 'left': this.x -= 101; break;
38       case 'right': this.x += 101; break;
39       case 'up': this.y -= 83; break;
40       case 'down': this.y += 83; break;
41    }
42 }

50 var allEnemies = [new Enemy(0, 83 * 2 + 55, 200)]
51 var player = new Player(101, 83 * 3 + 55)
52 // 什么??你说这些数字是什么意思根本看不懂?看不懂就对了!说明有痛点!这代码不够好,我们回头来重构。

以上是人物与动物移动的实现代码。实现了以后,人就可以上下左右移动了,虫子也可以具有速度一样飘移过去啦!虽然还会有些小 bug,比如人可以移出边框,虫子一飘出边界就走远了,等等。不过,千万不要在这里就急于把这些东西修复,理由有二:一是看到什么就想做什么,容易让思路跑掉,就算不跑,大脑也要在背后再开启一个线程“记住”你当前的核心工作其实是“实现碰撞检测”,它们都记在我们的任务列表里呢;二是当前价值更高的任务是实现碰撞检测,急于实现边缘功能不容易养成两个好习惯:任务分解排优先级一次只做一件事

那么下来我们来看下碰撞检测。这个功能的输入输出是什么呢?输入是 enemy 和 player 的位置,输出是一个结果:碰了还是没碰。这是高层次的划分模型。实现上,输入位置的数据结构可能待定;碰还是没碰,这是一个 boolean 值就可以表述的事。好,那么为了确定碰还是没碰,我们需要什么数据呢?不考虑太多技术细节和复杂模型(主要是我懒和图快…),“碰撞”了,那不就是 y 坐标一致的情况下,x 坐标碰一起,一样了么?所以,我们只需要两对 {x, y} 坐标,就足以判别需要的碰撞结果了。

如果我们是采用 TDD 开发,“碰撞检测”这个任务最后可能分解成这样的测试用例:

expect(checkCollision(new Enemy(row(1), y(whatever)), new Player(row(2), y(whatever))), is(false))
expect(checkCollision(new Enemy(row(2), y(300)), new Player(row(2), y(400))), is(false))
expect(checkCollision(new Enemy(row(2), y(300)), new Player(row(2), y(300))), is(true))

上面的代码描述了三种情况:

这也是我们说的代码即注释啊,多么简洁明了的(伪)代码。当然,这里我稍微深入了一下 TDD 和任务分解的内容。这两个思维工具/设计工具是我觉得威力极大的工具,有兴趣的同学可以参考下面的一些资料,除了第一个是我尝试写的,第二个和第三个都是最为精华的资料,其余的资料无需再多读:

好,扯远+安利了一下 TDD 以后,我们尝试来实现一下上面的测试用例。话说 TDD 在这个例子中我们较难做到(后面我会再讲),因此我们取其精髓——快速反馈——使用浏览器来得到反馈。前面说过,为什么要把碰撞的检测和复位分开呢?这里的优势就体现出来了,对于碰还是没碰的判定,我们可以单独通过 console.log() 打印出来。我们优先来实现最后一个用例,即碰撞的检测,因为这是核心功能中价值最高的用例。怎么实现呢?看下面代码与浏览器:

app.js 
23 Enemy.prototype.checkCollision = function (player) {
24    console.log(`check collision() is working, player: ${player.x}, ${player.y}`)
25 }

50 var allEnemies = [new Enemy(202, 83 * 2 + 55, 0)]
51 var player = new Player(202, 83 * 2 + 55)

image

我们先是强行把人物和虫子放在同一坐标,并把虫子的速度设置为0(折翼的瓢虫)。同时,这里我们先不急于着手实现功能,而是先写一个 log,确保:checkCollision() 被调用了,并且打印出了 player 的位置,说明传入的 player 的数据也是正确的。好,实验做到这,是时候开撸了。接下来,就是在 IDE 中写几行代码,然后马上在浏览器中看结果:

23 Enemy.prototype.checkCollision = function (player) {
24    if (this.y === player.y) {
25       console.log(`collision happened!! enemy.x: ${this.x}, player.x: ${player.x}`)
26    } else {
27       console.log(`player's safe!! enemy.x: ${this.x}, player.x: ${player.x}`)
28    }
29 }

image

嗯,没错,整个碰撞检测的实现代码就这么完成了。。。关于这个部分,其实实现起来还会有一些细节,比如如果等到 x 完全相同才判定为碰撞,那么它的发生概率其实是很低的,因此我们还需要考虑实现当两者“足够接近”时就判定为碰撞,也由此引申出了“碰撞半径”的问题。不过,我就把这个部分,以及上面尚未实现的两个测试用例,交给读者去探索发现啦。

到此我们的任务列表完成情况如下:

重构 - 精益求精的手艺 Refactor: The Craftsmanship

假定到这里为止,同学们的代码就都完成啦。如果遇到困难的话,可以寻求导师的帮助或者给我留言,我有看到都会尝试帮助。完成到这里,相信代码都是可以正常工作的了,也就是,达成 specification 的了。不过,如果你还想再进一步,还是有工作可做的。比如,我们现在的代码中,就有一些还让人看了不太明白,或者自我重复的代码,我列举几例如下:

41 Player.prototype.handleInput = function (movement) {
42    switch (movement) {
43       case 'left':
44          if (this.x >= 0) {
45             this.x -= 101;
46          } break;
47       case 'right':
48          if (this.x <= 404) {
49             this.x += 101;
50          } break;
51       case 'up':
52          if (this.y <= 55) {
53             this.y -= 83;
54          } break;
55       case 'down':
56         if (this.y >= 606) {
57             this.y += 83;
58          } break;
59    }
60 }

上面这个例子中,魔法数字太多,101,83,这些都是每个格子特定的长宽,而404则是整个屏幕的宽度,55、606更是通过某些神妙的计算得到的。这段代码的缺点有:

其实这段代码的意思很简单,用中文表达起来,无非就是:

  1. 如果能左转,那就左转
  2. 如果能右转,那就右转
  3. 如果能前进,那就前进
  4. 如果能后退,那就后退

是吧,多么清晰。那么我希望代码能变成这样:

41 Player.prototype.handleInput = function (movement) {
42   switch(movement) {
43     case 'left': if (player.canMoveLeft()) { this.moveLeft(); break; } 
44     case 'right': if (player.canMoveRight()) { this.moveRight(); break; }
45     case 'up': if (player.canMoveForward()) { this.moveForward(); break; }
46     case 'down': if (player.canMoveBackward()) { this.moveBackward(); break; }
47   }
48 }

如果是用 Java,还可以面向接口编程,相信在 JS 中也能做类似的实现:

public interface Command {
  boolean canMove();
  void move(Player player);
}

public class MoveLeftCommand implements Command {
  public MoveLeftCommand(Player player) { this.player = player; }

  @Override public boolean canMove() {
    return !GameBoard.exceedsLeftBorder(player);
  }

  @Override public void move() {
    return player.move(-oneCell, 0);
  } 
}

class Player {
  public void handleInput(movement) {
    Command command = CommandFactory.newCommand(movement, player);
    if (command.canMove()) {
      command.move()
    }
  } 
}

另外,还可以注意到,EnemyPlayer 的代码中存在很多重复,并且它们还具有一个完全相同的 render() 方法。

2  var Enemy = function(x, y, speed) {
3      this.x = x;
4      this.y = y;
5      this.speed = speed;
6      this.sprite = 'images/enemy-bug.png';
7  };

32 var Player = function (x, y) {
33    this.x = x;
34    this.y = y;
35    this.sprite = 'images/char-boy.png'
36 }
19 Enemy.prototype.render = function() {
20     ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
21 };

61 Player.prototype.render = function() {
62    ctx.drawImage(Resources.get(this.sprite), this.x, this.y);
63 };

这里的重复虽然不多,但是能重构之肯定是最好的。理想来说,我们希望把代码重构成这样:

var Player = function (x, y) {
   var movable = Movable(x, y, sprite);
}

var Enemy = function (x, y, speed) {
   var movable = Movable(x, y, sprite);
   movable.speed = speed;
}

var Movable = function(x, y, sprite) {
  ... 
}

然而,这个简单的事情在原生的 JS 里面做还是很坑的,因为 JS 原生并不支持多重继承和多层继承(有误处请指出)。光是尝试搜索和理解这个知识就花了我快两个小时的时间,而且我还不理解为什么代码能工作。这也是一个痛点,我们是用语言来写应用(application)的,花在语言特性本身上的时间——抛开你是想研究原理不说,这个应该是背后下功夫了——应该越少越好,我并不想我的应用代码中还有一些专门用于适配语言本身支持薄弱(如继承)的代码。

68 var allEnemies = [
69    new Enemy(22, 83 * 0 + 55, 20), new Enemy(21, 83 * 0 + 55, 23), // row 1
70    new Enemy(57, 83 * 1 + 55, 29), new Enemy(20, 83 * 1 + 55, 29), // row 2
71    new Enemy(22, 83 * 2 + 55, 50), new Enemy(59, 83 * 2 + 55, 50)  // row 3
72 ]
73 var player = new Player(202, 83 * 3 + 55)

还有上面这段代码,这段代码其实算还不错了,有注释指明初始化的 enemy 是在第几行,以及精心地排过版等。不过还是有优化空间,比如,EnemyPlayer 的初始化也不(fei)是(chang)很(bu)直观,这一大串数字都是什么意思呢?83 * 0/1/2/3 这些代码有什么规律吗?调速度在哪里调呢?调 x 坐标在哪里调呢?如果你想调整 y 坐标,恭喜你,你会发现碰撞检测的代码不工作了(目前我们的实现可不是通过比较“在不在同一行”,而是通过比较“y坐标是否相同”!

注释很好,它说明了代码很可能有重构空间,让我们尝试把注释消除,让代码自注释起来。目标,把代码给重构成这样:

var allEnemies = [
   new Enemy(randomPositionInRow(), GameBoard.row(1), Speed.FAST),
   new Enemy(randomPositionInRow(), GameBoard.row(1), Speed.VERY_FAST),
   new Enemy(randomPositionInRow(), GameBoard.row(1), Speed.SLOW),
   new Enemy(randomPositionInRow(), GameBoard.row(2), Speed.EXTREMELY_SLOW),
   new Enemy(randomPositionInRow(), GameBoard.row(2), Speed.EXTREMELY_FAST),
   new Enemy(randomPositionInRow(), GameBoard.row(3), Speed.NORMAL),
   new Enemy(randomPositionInRow(), GameBoard.row(3), Speed.EXTREMELY_SLOW)
];
var player = Player.initialPosition();

团队的发散模式 Peer Review

为什么 Code Review 很有用?因为无论经验多寡水平高低,他人总有其他的视角,能看到你看不到甚至不知道的东西。加上代码就表达了你的思路,别人也可以看到你思考的过程,从中学习,指出不足。这样你才能快速进步。

为什么别人总能看到你看不到的东西?如果你看过《学习之道》,你就会记得 Barbara 提到过,当你专注于解决一个特定问题的时候,你的大脑更倾向于使用“专注模式”(focused mode)调动全部的精力和逻辑来分析、解决它,这就导致了你的视野不广泛,就像手电筒🔦又长又窄的光束。视野不够宽,你很可能就会忽略周边的事物,忽略全局的配置,甚至可能找错方向一钻到底。这是由大脑的工作机理决定的,每个人都是一样的。

那么我怎么避免太过陷于专注模式而一叶障目不见泰山呢?有两种方式:一种是通过自己经常性地自律自省,一次不要学习太长时间,经常放松,让大脑有休息、切换到发散模式重新审视全局的机会;另一种,就是借助他人或团队的力量把你拉出来,从不同视角、水平、经验激发你,比如加入兴趣小组、讨论会、头脑风暴等。而 Code Review 作为专注于代码的团队活动,则是更高效的方式,他人会从整体的视角来阅读理解你的代码,指出其中的优点与不足。

Code Review 的时候我就收到了许多有效的反馈,比如我漏了很多空格,这个可以通过一些 JSHint/ESLint/Airbnb 的工具来自动化检查;比如我误解了需求中关于胜利的描述(其实,按照敏捷的方式我应该读完题目后不懂就在群里发问或者问导师的哈哈哈)等。同时,代码中写得好的地方被肯定,也是很开心的事。因为开心,所以更愿意去学习,更愿意去分享出来,甚至尝试去教别人,我想,这也是社区学习最有优势的地方吧,要好好利用啊。又想扯点别的,当是给这门课程打打广告了,哈哈哈。嗯,我觉得这种课程和社区学习最大的亮点在于:

呼,总算把扯远的扯回来了。

完成时间分析 Time Entries Analyse

为什么要做完成时间分析呢?套导师们之前分享的成长型思维来说呢,就是学习它更多不是一个会不会的结果问题,而是一个不断进步成长的过程。我们怎么让我们底层的技能更加熟练,从而能更快更好地完成更高层的任务呢?《刻意练习》一书中给出的回答是,每次要挑战比自己的舒适区超过一点点的难度,少则没用,多则易折。那么怎么来衡量你的舒适区和刚好超过一点的极限点在哪里呢?

时间分析,可以为你完成某个任务的时间提供数据支撑(我真是机器型思维啊哭)。比如这个项目你觉得很简单,5小时就做完了,那能不能挑战4小时做完呢?4小时能做完,能不能挑战3小时呢?你的极限是多少呢?通过这样的训练,可以帮助你发现自己在任务分解、编码速度过程中的优缺点,帮助你发现任务分解做的不好、快捷键不熟练、单项技术不熟练等问题,从而针对性地刻意练习提高。

我给自己做的这个游戏项目的完成时间分析如下:

Lesson Description Duration Total
P1 - Arcade Game 阅读项目要求 26:34
任务分解 22:19
让游戏先跑起来 26:31
实现基本逻辑 1:23:34
重构代码 1:38:29
理解原型类两次继承原理及实现 2:12:59
添加测试及测试设施 15:03
实现胜利后效果 27:00
撰写 README 18:44
对齐 Styleguide 17:56
作业提交 09:40
使用 Gravatar 链接 WordPress 来在优达中显示头像 23:14
收到 Code Review,修改代码、查阅资料 1:24:14 9h 46min
产出博客 5h 20min 15h 6min

可以发现什么呢?

这个时间分析表,又引出了下一节——也是最后一节——要讲的内容:如何通过刻意练习提高,以及 ES5本身的痛点(原生不支持继承)及解决方案。

image

image

展望 The Future

这个题目完成到这里,基本就结束了,博客也快要接近尾声。不过,对于散乱地遗失在文章中各处的一些痛点和线索,在这里我们再做一个归纳,看从功能和实践上,有哪些事情还可以做得更好。

ES5 的痛点 - ES2015: New Generation of JavaScript

ES5 是 JavaScript 使用时间很长的一个规范,可以说在 ES6(ES2015) 出来之前,我们打交道最多的就是 ES5 了。JavaScript 作为函数式编程语言,相对静态语言有其独特的优点,比如 函数式编程、闭包(更灵活的数据封装级别)、弱数据类型等。同时,其缺点也很明显,比如 没有模块管理、弱类型有时不可预测 IDE 无法补全、语言有一些奇奇怪怪的特性,等。为此 Douglas 专门还写过一本书,叫 JavaScript: The Good Parts 来阐述 JS 优美的特性和糟粕特性,并尝试归纳出一个建议的最佳实践。在这个项目中,我感觉最坑的部分,是自己要为继承设置原型链的部分。

好在,老道讲的 ES5 中的很多不好的特性在 ES6 中都已经提供了极为优雅的实现,并且大部分浏览器也已经支持了 ES6 的大部分特性(至少我首选的 Chrome 已支持哇咔咔)。因此,接下来,我会尝试使用 ES6 来实现这个游戏,看看时间方面能减少多少功夫。

快速反馈 - TDD with ES6 in JavaScript

文章还提到另一个痛点是说,在这个项目里我很难用上 TDD(测试驱动开发)。为什么会这样呢?主要原因有三:

升级到 ES6 可以解决这个问题。当然,关于没有模块这个问题肯定还有其他的解决方案,不过就我可见的能力和经验而言,使用 ES6 对我来说是最简单的方式。有更好的方式,请在下方给我留言指出!!谢谢~

刻意练习 - Two Times Quicker

上一节产出了一个完成时间分析,其中我们可以看到,核心代码的完成用了4个小时左右,基础设施代码的搭建可能花了1.5小时左右。那么,在下一次练习中,我将尝试使用 ES6+TDD 来重写代码,看是否能把总体时间缩短到4个小时,并进一步练习。顺便预告一下下篇博客:刻意练习。哈哈哈。

结稿撒花🌼💐🌸🌺🌹🌻🌷💮。


isbaselvy commented 6 years ago

感谢提供的思路,豁然开朗